conversation_service.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. package service
  2. import (
  3. "errors"
  4. "strings"
  5. "time"
  6. "github.com/2930134478/AI-CS/backend/models"
  7. "github.com/2930134478/AI-CS/backend/repository"
  8. "gorm.io/gorm"
  9. )
  10. // ConversationService 负责会话领域的业务编排。
  11. type ConversationService struct {
  12. conversations *repository.ConversationRepository
  13. messages *repository.MessageRepository
  14. aiConfigRepo *repository.AIConfigRepository // 用于验证 AI 配置
  15. userRepo *repository.UserRepository // 用于查询用户设置
  16. }
  17. // NewConversationService 创建 ConversationService 实例。
  18. func NewConversationService(
  19. conversations *repository.ConversationRepository,
  20. messages *repository.MessageRepository,
  21. aiConfigRepo *repository.AIConfigRepository,
  22. userRepo *repository.UserRepository,
  23. ) *ConversationService {
  24. return &ConversationService{
  25. conversations: conversations,
  26. messages: messages,
  27. aiConfigRepo: aiConfigRepo,
  28. userRepo: userRepo,
  29. }
  30. }
  31. // InitConversation 为访客创建或恢复会话。
  32. func (s *ConversationService) InitConversation(input InitConversationInput) (*InitConversationResult, error) {
  33. var (
  34. conv *models.Conversation
  35. err error
  36. )
  37. conv, err = s.conversations.FindOpenByVisitorID(input.VisitorID)
  38. isNewConversation := false
  39. if err != nil {
  40. if errors.Is(err, gorm.ErrRecordNotFound) {
  41. now := time.Now()
  42. chatMode := input.ChatMode
  43. if chatMode == "" {
  44. chatMode = "human" // 默认人工客服
  45. }
  46. // 如果是 AI 模式,验证 AI 配置
  47. var aiConfigID *uint
  48. if chatMode == "ai" {
  49. if input.AIConfigID == nil || *input.AIConfigID == 0 {
  50. return nil, errors.New("AI 模式需要选择模型配置")
  51. }
  52. // 验证配置是否存在且开放
  53. config, err := s.aiConfigRepo.GetByID(*input.AIConfigID)
  54. if err != nil {
  55. return nil, errors.New("模型配置不存在")
  56. }
  57. if !config.IsPublic {
  58. return nil, errors.New("该模型未开放给访客使用")
  59. }
  60. if !config.IsActive {
  61. return nil, errors.New("该模型配置已禁用")
  62. }
  63. aiConfigID = input.AIConfigID
  64. }
  65. conv = &models.Conversation{
  66. ConversationType: "visitor",
  67. VisitorID: input.VisitorID,
  68. Status: "open",
  69. Website: input.Website,
  70. Referrer: input.Referrer,
  71. Browser: input.Browser,
  72. OS: input.OS,
  73. Language: input.Language,
  74. IPAddress: input.IPAddress,
  75. LastSeenAt: &now,
  76. ChatMode: chatMode,
  77. AIConfigID: aiConfigID,
  78. }
  79. if err := s.conversations.Create(conv); err != nil {
  80. return nil, err
  81. }
  82. isNewConversation = true
  83. } else {
  84. return nil, err
  85. }
  86. } else {
  87. // 恢复已存在的对话
  88. now := time.Now()
  89. updates := map[string]interface{}{
  90. "last_seen_at": &now,
  91. }
  92. // 更新访客信息(如果之前没有)
  93. if input.Website != "" && conv.Website == "" {
  94. updates["website"] = input.Website
  95. }
  96. if input.Referrer != "" && conv.Referrer == "" {
  97. updates["referrer"] = input.Referrer
  98. }
  99. if input.Browser != "" && conv.Browser == "" {
  100. updates["browser"] = input.Browser
  101. }
  102. if input.OS != "" && conv.OS == "" {
  103. updates["os"] = input.OS
  104. }
  105. if input.Language != "" && conv.Language == "" {
  106. updates["language"] = input.Language
  107. }
  108. if input.IPAddress != "" && conv.IPAddress == "" {
  109. updates["ip_address"] = input.IPAddress
  110. }
  111. // 重要:如果用户选择了新的 ChatMode,更新对话模式
  112. // 这样访客可以在人工客服和 AI 客服之间切换
  113. if input.ChatMode != "" && input.ChatMode != conv.ChatMode {
  114. chatMode := input.ChatMode
  115. updates["chat_mode"] = chatMode
  116. // 如果是 AI 模式,验证并更新 AI 配置
  117. if chatMode == "ai" {
  118. if input.AIConfigID == nil || *input.AIConfigID == 0 {
  119. return nil, errors.New("AI 模式需要选择模型配置")
  120. }
  121. // 验证配置是否存在且开放
  122. config, err := s.aiConfigRepo.GetByID(*input.AIConfigID)
  123. if err != nil {
  124. return nil, errors.New("模型配置不存在")
  125. }
  126. if !config.IsPublic {
  127. return nil, errors.New("该模型未开放给访客使用")
  128. }
  129. if !config.IsActive {
  130. return nil, errors.New("该模型配置已禁用")
  131. }
  132. updates["ai_config_id"] = input.AIConfigID
  133. } else {
  134. // 切换到人工客服模式,清除 AI 配置
  135. updates["ai_config_id"] = nil
  136. }
  137. }
  138. if err := s.conversations.UpdateFields(conv.ID, updates); err != nil {
  139. return nil, err
  140. }
  141. // 重新获取更新后的对话信息
  142. conv, err = s.conversations.GetByID(conv.ID)
  143. if err != nil {
  144. return nil, err
  145. }
  146. }
  147. if isNewConversation {
  148. now := time.Now()
  149. chatMode := input.ChatMode
  150. if chatMode == "" {
  151. chatMode = "human" // 默认人工模式
  152. }
  153. message := &models.Message{
  154. ConversationID: conv.ID,
  155. SenderID: 0,
  156. SenderIsAgent: false,
  157. Content: "Visitor opened the page",
  158. MessageType: "system_message",
  159. ChatMode: chatMode, // 记录系统消息发送时的对话模式
  160. IsRead: true,
  161. ReadAt: &now,
  162. }
  163. if input.Website != "" {
  164. message.Content += " [" + input.Website + "]"
  165. }
  166. if err := s.messages.Create(message); err != nil {
  167. return nil, err
  168. }
  169. if input.Referrer != "" {
  170. readTime := time.Now()
  171. chatMode := input.ChatMode
  172. if chatMode == "" {
  173. chatMode = "human" // 默认人工模式
  174. }
  175. referrerMsg := &models.Message{
  176. ConversationID: conv.ID,
  177. SenderID: 0,
  178. SenderIsAgent: false,
  179. Content: "Visitor came from [" + input.Referrer + "]",
  180. MessageType: "system_message",
  181. ChatMode: chatMode, // 记录系统消息发送时的对话模式
  182. IsRead: true,
  183. ReadAt: &readTime,
  184. }
  185. if err := s.messages.Create(referrerMsg); err != nil {
  186. return nil, err
  187. }
  188. }
  189. }
  190. return &InitConversationResult{
  191. ConversationID: conv.ID,
  192. Status: conv.Status,
  193. }, nil
  194. }
  195. // UpdateConversationContact 更新访客的联系信息(邮箱、电话、备注)。
  196. func (s *ConversationService) UpdateConversationContact(input UpdateConversationContactInput) (*ConversationDetail, error) {
  197. if _, err := s.conversations.GetByID(input.ConversationID); err != nil {
  198. if errors.Is(err, gorm.ErrRecordNotFound) {
  199. return nil, ErrConversationNotFound
  200. }
  201. return nil, err
  202. }
  203. updates := map[string]interface{}{}
  204. if input.Email != nil {
  205. updates["email"] = strings.TrimSpace(*input.Email)
  206. }
  207. if input.Phone != nil {
  208. updates["phone"] = strings.TrimSpace(*input.Phone)
  209. }
  210. if input.Notes != nil {
  211. updates["notes"] = strings.TrimSpace(*input.Notes)
  212. }
  213. if err := s.conversations.UpdateFields(input.ConversationID, updates); err != nil {
  214. return nil, err
  215. }
  216. // UpdateConversationContact 不传递 userID,因为更新联系信息时不需要检查参与状态
  217. return s.GetConversationDetail(input.ConversationID, 0)
  218. }
  219. func (s *ConversationService) buildSummary(conv models.Conversation, userID uint) (ConversationSummary, error) {
  220. var lastSeen *time.Time
  221. if conv.LastSeenAt != nil {
  222. lastSeen = conv.LastSeenAt
  223. }
  224. // 检查当前用户是否参与过该会话(是否发送过消息)
  225. hasParticipated := false
  226. if userID > 0 {
  227. if participated, err := s.messages.HasAgentParticipated(conv.ID, userID); err == nil {
  228. hasParticipated = participated
  229. }
  230. // 错误时静默处理,不影响流程
  231. }
  232. summary := ConversationSummary{
  233. ID: conv.ID,
  234. ConversationType: conv.ConversationType,
  235. VisitorID: conv.VisitorID,
  236. AgentID: conv.AgentID,
  237. Status: conv.Status,
  238. ChatMode: conv.ChatMode,
  239. CreatedAt: conv.CreatedAt,
  240. UpdatedAt: conv.UpdatedAt,
  241. LastSeenAt: lastSeen,
  242. HasParticipated: hasParticipated,
  243. }
  244. if message, err := s.messages.LatestByConversationID(conv.ID); err == nil && message != nil {
  245. var readAt *time.Time
  246. if message.ReadAt != nil {
  247. readAt = message.ReadAt
  248. }
  249. summary.LastMessage = &LastMessageSummary{
  250. ID: message.ID,
  251. Content: message.Content,
  252. SenderIsAgent: message.SenderIsAgent,
  253. MessageType: message.MessageType,
  254. IsRead: message.IsRead,
  255. ReadAt: readAt,
  256. CreatedAt: message.CreatedAt,
  257. }
  258. }
  259. if count, err := s.messages.CountUnreadBySender(conv.ID, false); err == nil {
  260. summary.UnreadCount = count
  261. }
  262. return summary, nil
  263. }
  264. // ListConversations 返回当前活跃会话的摘要信息。
  265. // userID: 当前登录的客服ID(可选,如果为0则使用默认过滤规则)
  266. // 过滤规则:
  267. // 1. 默认不显示 ChatMode == "ai" 的对话
  268. // 2. 如果 userID > 0 且该用户的 ReceiveAIConversations == false,则不显示 AI 对话
  269. // 3. 只显示 ChatMode == "human" 且存在访客消息的对话(访客切换到人工并发送消息后)
  270. func (s *ConversationService) ListConversations(userID uint) ([]ConversationSummary, error) {
  271. conversations, err := s.conversations.ListActive()
  272. if err != nil {
  273. return nil, err
  274. }
  275. result := make([]ConversationSummary, 0, len(conversations))
  276. for _, conv := range conversations {
  277. // 过滤规则 1: 默认不显示 AI 对话
  278. // 只有在会话页面手动开启"显示 AI 对话"时才显示
  279. if conv.ChatMode == "ai" {
  280. continue
  281. }
  282. // 过滤规则 2: 如果是人工对话,检查是否有访客发送的消息
  283. // 只有当访客切换到人工并发送消息后,才显示在列表中
  284. if conv.ChatMode == "human" {
  285. hasVisitorMessage, err := s.messages.HasVisitorMessageInHumanMode(conv.ID)
  286. if err != nil {
  287. // 如果查询失败,为了安全起见,不显示该对话
  288. continue
  289. }
  290. if !hasVisitorMessage {
  291. // 没有访客消息,不显示(访客只是切换了模式,但还没发送消息)
  292. continue
  293. }
  294. }
  295. // 通过过滤,添加到结果列表
  296. summary, err := s.buildSummary(conv, userID)
  297. if err != nil {
  298. continue // 如果构建摘要失败,跳过该对话
  299. }
  300. result = append(result, summary)
  301. }
  302. return result, nil
  303. }
  304. // GetConversationDetail 获取指定会话的详细信息。内部对话仅创建者(agent_id)可查看。
  305. func (s *ConversationService) GetConversationDetail(id uint, userID uint) (*ConversationDetail, error) {
  306. conv, err := s.conversations.GetByID(id)
  307. if err != nil {
  308. return nil, err
  309. }
  310. if conv.ConversationType == "internal" && userID > 0 && conv.AgentID != userID {
  311. return nil, gorm.ErrRecordNotFound
  312. }
  313. summary, err := s.buildSummary(*conv, userID)
  314. if err != nil {
  315. return nil, err
  316. }
  317. var lastSeen *time.Time
  318. if conv.LastSeenAt != nil {
  319. lastSeen = conv.LastSeenAt
  320. }
  321. return &ConversationDetail{
  322. ConversationSummary: summary,
  323. Website: conv.Website,
  324. Referrer: conv.Referrer,
  325. Browser: conv.Browser,
  326. OS: conv.OS,
  327. Language: conv.Language,
  328. IPAddress: conv.IPAddress,
  329. Location: conv.Location,
  330. Email: conv.Email,
  331. Phone: conv.Phone,
  332. Notes: conv.Notes,
  333. LastSeen: lastSeen,
  334. }, nil
  335. }
  336. // SearchConversations 根据关键字检索会话摘要。
  337. // userID: 当前登录的客服ID(可选,用于检查参与状态)
  338. func (s *ConversationService) SearchConversations(query string, userID uint) ([]ConversationSummary, error) {
  339. pattern := "%" + query + "%"
  340. idSet := map[uint]struct{}{}
  341. if ids, err := s.messages.FindConversationIDsByContent(pattern); err == nil {
  342. for _, id := range ids {
  343. idSet[id] = struct{}{}
  344. }
  345. } else {
  346. return nil, err
  347. }
  348. if convs, err := s.conversations.SearchByIDOrVisitorLike(pattern); err == nil {
  349. for _, conv := range convs {
  350. idSet[conv.ID] = struct{}{}
  351. }
  352. } else {
  353. return nil, err
  354. }
  355. if len(idSet) == 0 {
  356. return []ConversationSummary{}, nil
  357. }
  358. ids := make([]uint, 0, len(idSet))
  359. for id := range idSet {
  360. ids = append(ids, id)
  361. }
  362. conversations, err := s.conversations.ListByIDs(ids)
  363. if err != nil {
  364. return nil, err
  365. }
  366. result := make([]ConversationSummary, 0, len(conversations))
  367. for _, conv := range conversations {
  368. summary, err := s.buildSummary(conv, userID)
  369. if err != nil {
  370. return nil, err
  371. }
  372. result = append(result, summary)
  373. }
  374. return result, nil
  375. }
  376. // UpdateVisitorOnlineStatus 更新访客在线状态和最后活跃时间。
  377. // 当 isOnline 为 true 时,更新 last_seen_at 为当前时间,并确保状态为 "open"。
  378. // 当 isOnline 为 false 时,仅更新 last_seen_at 为当前时间,不改变状态。
  379. func (s *ConversationService) UpdateVisitorOnlineStatus(conversationID uint, isOnline bool) error {
  380. now := time.Now()
  381. updates := map[string]interface{}{
  382. "last_seen_at": &now,
  383. }
  384. // 如果标记为在线,确保状态为 "open"(但不要将已关闭的会话重新打开)
  385. if isOnline {
  386. conv, err := s.conversations.GetByID(conversationID)
  387. if err != nil {
  388. return err
  389. }
  390. // 只有当前状态不是 "closed" 时,才更新为 "open"
  391. if conv.Status != "closed" {
  392. updates["status"] = "open"
  393. }
  394. }
  395. return s.conversations.UpdateFields(conversationID, updates)
  396. }
  397. // UpdateLastSeenAt 更新访客的最后活跃时间。
  398. func (s *ConversationService) UpdateLastSeenAt(conversationID uint) error {
  399. now := time.Now()
  400. return s.conversations.UpdateFields(conversationID, map[string]interface{}{
  401. "last_seen_at": &now,
  402. })
  403. }
  404. // InitInternalConversation 为客服创建一条新的内部对话(知识库测试用)。每次调用创建新会话。
  405. func (s *ConversationService) InitInternalConversation(agentID uint) (*InitConversationResult, error) {
  406. if agentID == 0 {
  407. return nil, errors.New("agent_id is required for internal conversation")
  408. }
  409. conv := &models.Conversation{
  410. ConversationType: "internal",
  411. VisitorID: 0,
  412. AgentID: agentID,
  413. Status: "open",
  414. ChatMode: "ai",
  415. }
  416. if err := s.conversations.Create(conv); err != nil {
  417. return nil, err
  418. }
  419. return &InitConversationResult{
  420. ConversationID: conv.ID,
  421. Status: conv.Status,
  422. }, nil
  423. }
  424. // ListInternalConversations 返回当前客服的全部内部对话(知识库测试用)。
  425. func (s *ConversationService) ListInternalConversations(agentID uint) ([]ConversationSummary, error) {
  426. if agentID == 0 {
  427. return []ConversationSummary{}, nil
  428. }
  429. conversations, err := s.conversations.ListActiveInternalByAgentID(agentID)
  430. if err != nil {
  431. return nil, err
  432. }
  433. result := make([]ConversationSummary, 0, len(conversations))
  434. for _, conv := range conversations {
  435. summary, err := s.buildSummary(conv, agentID)
  436. if err != nil {
  437. continue
  438. }
  439. result = append(result, summary)
  440. }
  441. return result, nil
  442. }