conversation_service.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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. }
  15. // NewConversationService 创建 ConversationService 实例。
  16. func NewConversationService(
  17. conversations *repository.ConversationRepository,
  18. messages *repository.MessageRepository,
  19. ) *ConversationService {
  20. return &ConversationService{
  21. conversations: conversations,
  22. messages: messages,
  23. }
  24. }
  25. // InitConversation 为访客创建或恢复会话。
  26. func (s *ConversationService) InitConversation(input InitConversationInput) (*InitConversationResult, error) {
  27. var (
  28. conv *models.Conversation
  29. err error
  30. )
  31. conv, err = s.conversations.FindOpenByVisitorID(input.VisitorID)
  32. isNewConversation := false
  33. if err != nil {
  34. if errors.Is(err, gorm.ErrRecordNotFound) {
  35. now := time.Now()
  36. conv = &models.Conversation{
  37. VisitorID: input.VisitorID,
  38. Status: "open",
  39. Website: input.Website,
  40. Referrer: input.Referrer,
  41. Browser: input.Browser,
  42. OS: input.OS,
  43. Language: input.Language,
  44. IPAddress: input.IPAddress,
  45. LastSeenAt: &now,
  46. }
  47. if err := s.conversations.Create(conv); err != nil {
  48. return nil, err
  49. }
  50. isNewConversation = true
  51. } else {
  52. return nil, err
  53. }
  54. } else {
  55. now := time.Now()
  56. updates := map[string]interface{}{
  57. "last_seen_at": &now,
  58. }
  59. if input.Website != "" && conv.Website == "" {
  60. updates["website"] = input.Website
  61. }
  62. if input.Referrer != "" && conv.Referrer == "" {
  63. updates["referrer"] = input.Referrer
  64. }
  65. if input.Browser != "" && conv.Browser == "" {
  66. updates["browser"] = input.Browser
  67. }
  68. if input.OS != "" && conv.OS == "" {
  69. updates["os"] = input.OS
  70. }
  71. if input.Language != "" && conv.Language == "" {
  72. updates["language"] = input.Language
  73. }
  74. if input.IPAddress != "" && conv.IPAddress == "" {
  75. updates["ip_address"] = input.IPAddress
  76. }
  77. if err := s.conversations.UpdateFields(conv.ID, updates); err != nil {
  78. return nil, err
  79. }
  80. }
  81. if isNewConversation {
  82. now := time.Now()
  83. message := &models.Message{
  84. ConversationID: conv.ID,
  85. SenderID: 0,
  86. SenderIsAgent: false,
  87. Content: "Visitor opened the page",
  88. MessageType: "system_message",
  89. IsRead: true,
  90. ReadAt: &now,
  91. }
  92. if input.Website != "" {
  93. message.Content += " [" + input.Website + "]"
  94. }
  95. if err := s.messages.Create(message); err != nil {
  96. return nil, err
  97. }
  98. if input.Referrer != "" {
  99. readTime := time.Now()
  100. referrerMsg := &models.Message{
  101. ConversationID: conv.ID,
  102. SenderID: 0,
  103. SenderIsAgent: false,
  104. Content: "Visitor came from [" + input.Referrer + "]",
  105. MessageType: "system_message",
  106. IsRead: true,
  107. ReadAt: &readTime,
  108. }
  109. if err := s.messages.Create(referrerMsg); err != nil {
  110. return nil, err
  111. }
  112. }
  113. }
  114. return &InitConversationResult{
  115. ConversationID: conv.ID,
  116. Status: conv.Status,
  117. }, nil
  118. }
  119. // UpdateConversationContact 更新访客的联系信息(邮箱、电话、备注)。
  120. func (s *ConversationService) UpdateConversationContact(input UpdateConversationContactInput) (*ConversationDetail, error) {
  121. if _, err := s.conversations.GetByID(input.ConversationID); err != nil {
  122. if errors.Is(err, gorm.ErrRecordNotFound) {
  123. return nil, ErrConversationNotFound
  124. }
  125. return nil, err
  126. }
  127. updates := map[string]interface{}{}
  128. if input.Email != nil {
  129. updates["email"] = strings.TrimSpace(*input.Email)
  130. }
  131. if input.Phone != nil {
  132. updates["phone"] = strings.TrimSpace(*input.Phone)
  133. }
  134. if input.Notes != nil {
  135. updates["notes"] = strings.TrimSpace(*input.Notes)
  136. }
  137. if err := s.conversations.UpdateFields(input.ConversationID, updates); err != nil {
  138. return nil, err
  139. }
  140. return s.GetConversationDetail(input.ConversationID)
  141. }
  142. func (s *ConversationService) buildSummary(conv models.Conversation) (ConversationSummary, error) {
  143. var lastSeen *time.Time
  144. if conv.LastSeenAt != nil {
  145. lastSeen = conv.LastSeenAt
  146. }
  147. summary := ConversationSummary{
  148. ID: conv.ID,
  149. VisitorID: conv.VisitorID,
  150. AgentID: conv.AgentID,
  151. Status: conv.Status,
  152. CreatedAt: conv.CreatedAt,
  153. UpdatedAt: conv.UpdatedAt,
  154. LastSeenAt: lastSeen, // 添加 last_seen_at 字段
  155. }
  156. if message, err := s.messages.LatestByConversationID(conv.ID); err == nil && message != nil {
  157. var readAt *time.Time
  158. if message.ReadAt != nil {
  159. readAt = message.ReadAt
  160. }
  161. summary.LastMessage = &LastMessageSummary{
  162. ID: message.ID,
  163. Content: message.Content,
  164. SenderIsAgent: message.SenderIsAgent,
  165. MessageType: message.MessageType,
  166. IsRead: message.IsRead,
  167. ReadAt: readAt,
  168. CreatedAt: message.CreatedAt,
  169. }
  170. }
  171. if count, err := s.messages.CountUnreadBySender(conv.ID, false); err == nil {
  172. summary.UnreadCount = count
  173. }
  174. return summary, nil
  175. }
  176. // ListConversations 返回当前活跃会话的摘要信息。
  177. func (s *ConversationService) ListConversations() ([]ConversationSummary, error) {
  178. conversations, err := s.conversations.ListActive()
  179. if err != nil {
  180. return nil, err
  181. }
  182. result := make([]ConversationSummary, 0, len(conversations))
  183. for _, conv := range conversations {
  184. summary, err := s.buildSummary(conv)
  185. if err != nil {
  186. return nil, err
  187. }
  188. result = append(result, summary)
  189. }
  190. return result, nil
  191. }
  192. // GetConversationDetail 获取指定会话的详细信息。
  193. func (s *ConversationService) GetConversationDetail(id uint) (*ConversationDetail, error) {
  194. conv, err := s.conversations.GetByID(id)
  195. if err != nil {
  196. return nil, err
  197. }
  198. summary, err := s.buildSummary(*conv)
  199. if err != nil {
  200. return nil, err
  201. }
  202. var lastSeen *time.Time
  203. if conv.LastSeenAt != nil {
  204. lastSeen = conv.LastSeenAt
  205. }
  206. return &ConversationDetail{
  207. ConversationSummary: summary,
  208. Website: conv.Website,
  209. Referrer: conv.Referrer,
  210. Browser: conv.Browser,
  211. OS: conv.OS,
  212. Language: conv.Language,
  213. IPAddress: conv.IPAddress,
  214. Location: conv.Location,
  215. Email: conv.Email,
  216. Phone: conv.Phone,
  217. Notes: conv.Notes,
  218. LastSeen: lastSeen,
  219. }, nil
  220. }
  221. // SearchConversations 根据关键字检索会话摘要。
  222. func (s *ConversationService) SearchConversations(query string) ([]ConversationSummary, error) {
  223. pattern := "%" + query + "%"
  224. idSet := map[uint]struct{}{}
  225. if ids, err := s.messages.FindConversationIDsByContent(pattern); err == nil {
  226. for _, id := range ids {
  227. idSet[id] = struct{}{}
  228. }
  229. } else {
  230. return nil, err
  231. }
  232. if convs, err := s.conversations.SearchByIDOrVisitorLike(pattern); err == nil {
  233. for _, conv := range convs {
  234. idSet[conv.ID] = struct{}{}
  235. }
  236. } else {
  237. return nil, err
  238. }
  239. if len(idSet) == 0 {
  240. return []ConversationSummary{}, nil
  241. }
  242. ids := make([]uint, 0, len(idSet))
  243. for id := range idSet {
  244. ids = append(ids, id)
  245. }
  246. conversations, err := s.conversations.ListByIDs(ids)
  247. if err != nil {
  248. return nil, err
  249. }
  250. result := make([]ConversationSummary, 0, len(conversations))
  251. for _, conv := range conversations {
  252. summary, err := s.buildSummary(conv)
  253. if err != nil {
  254. return nil, err
  255. }
  256. result = append(result, summary)
  257. }
  258. return result, nil
  259. }
  260. // UpdateVisitorOnlineStatus 更新访客在线状态和最后活跃时间。
  261. // 当 isOnline 为 true 时,更新 last_seen_at 为当前时间,并确保状态为 "open"。
  262. // 当 isOnline 为 false 时,仅更新 last_seen_at 为当前时间,不改变状态。
  263. func (s *ConversationService) UpdateVisitorOnlineStatus(conversationID uint, isOnline bool) error {
  264. now := time.Now()
  265. updates := map[string]interface{}{
  266. "last_seen_at": &now,
  267. }
  268. // 如果标记为在线,确保状态为 "open"(但不要将已关闭的会话重新打开)
  269. if isOnline {
  270. conv, err := s.conversations.GetByID(conversationID)
  271. if err != nil {
  272. return err
  273. }
  274. // 只有当前状态不是 "closed" 时,才更新为 "open"
  275. if conv.Status != "closed" {
  276. updates["status"] = "open"
  277. }
  278. }
  279. return s.conversations.UpdateFields(conversationID, updates)
  280. }
  281. // UpdateLastSeenAt 更新访客的最后活跃时间。
  282. func (s *ConversationService) UpdateLastSeenAt(conversationID uint) error {
  283. now := time.Now()
  284. return s.conversations.UpdateFields(conversationID, map[string]interface{}{
  285. "last_seen_at": &now,
  286. })
  287. }