conversation_controller.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. package controller
  2. import (
  3. "net/http"
  4. "strconv"
  5. "github.com/2930134478/AI-CS/backend/service"
  6. "github.com/2930134478/AI-CS/backend/utils"
  7. "github.com/gin-gonic/gin"
  8. )
  9. // ConversationController 负责处理会话相关的 HTTP 请求。
  10. type ConversationController struct {
  11. conversationService *service.ConversationService
  12. aiConfigService *service.AIConfigService // 用于获取开放的模型列表
  13. }
  14. // NewConversationController 创建 ConversationController 实例。
  15. func NewConversationController(
  16. conversationService *service.ConversationService,
  17. aiConfigService *service.AIConfigService,
  18. ) *ConversationController {
  19. return &ConversationController{
  20. conversationService: conversationService,
  21. aiConfigService: aiConfigService,
  22. }
  23. }
  24. type initConversationRequest struct {
  25. VisitorID uint `json:"visitor_id"`
  26. Website string `json:"website"`
  27. Referrer string `json:"referrer"`
  28. Browser string `json:"browser"`
  29. OS string `json:"os"`
  30. Language string `json:"language"`
  31. ChatMode string `json:"chat_mode"` // 对话模式:human(人工客服)、ai(AI客服)
  32. AIConfigID *uint `json:"ai_config_id"` // AI 配置 ID(访客选择的模型配置,AI 模式时必需)
  33. }
  34. type updateContactRequest struct {
  35. Email *string `json:"email"`
  36. Phone *string `json:"phone"`
  37. Notes *string `json:"notes"`
  38. }
  39. // InitConversation 为访客初始化或恢复会话。
  40. func (cc *ConversationController) InitConversation(c *gin.Context) {
  41. var req initConversationRequest
  42. if err := c.ShouldBindJSON(&req); err != nil || req.VisitorID == 0 {
  43. c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
  44. return
  45. }
  46. browser := req.Browser
  47. os := req.OS
  48. if browser == "" || os == "" {
  49. parsedBrowser, parsedOS := utils.ParseUserAgent(c.GetHeader("User-Agent"))
  50. if browser == "" {
  51. browser = parsedBrowser
  52. }
  53. if os == "" {
  54. os = parsedOS
  55. }
  56. }
  57. result, err := cc.conversationService.InitConversation(service.InitConversationInput{
  58. VisitorID: req.VisitorID,
  59. Website: req.Website,
  60. Referrer: req.Referrer,
  61. Browser: browser,
  62. OS: os,
  63. Language: req.Language,
  64. IPAddress: utils.GetClientIP(c),
  65. ChatMode: req.ChatMode,
  66. AIConfigID: req.AIConfigID,
  67. })
  68. if err != nil {
  69. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  70. return
  71. }
  72. c.JSON(http.StatusOK, gin.H{
  73. "conversation_id": result.ConversationID,
  74. "status": result.Status,
  75. })
  76. }
  77. // InitInternalConversation 为当前客服创建一条新的内部对话(知识库测试)。需要 query user_id。
  78. func (cc *ConversationController) InitInternalConversation(c *gin.Context) {
  79. userIDStr := c.Query("user_id")
  80. if userIDStr == "" {
  81. c.JSON(http.StatusBadRequest, gin.H{"error": "需要 user_id"})
  82. return
  83. }
  84. userID, err := strconv.ParseUint(userIDStr, 10, 32)
  85. if err != nil || userID == 0 {
  86. c.JSON(http.StatusBadRequest, gin.H{"error": "user_id 不合法"})
  87. return
  88. }
  89. result, err := cc.conversationService.InitInternalConversation(uint(userID))
  90. if err != nil {
  91. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  92. return
  93. }
  94. c.JSON(http.StatusOK, gin.H{
  95. "conversation_id": result.ConversationID,
  96. "status": result.Status,
  97. })
  98. }
  99. // GetPublicAIModels 获取所有开放的模型配置(供访客选择)。
  100. func (cc *ConversationController) GetPublicAIModels(c *gin.Context) {
  101. modelType := c.DefaultQuery("model_type", "text")
  102. models, err := cc.aiConfigService.GetPublicModels(modelType)
  103. if err != nil {
  104. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  105. return
  106. }
  107. c.JSON(http.StatusOK, gin.H{"models": models})
  108. }
  109. // UpdateContactInfo 用于更新访客的联系信息。
  110. func (cc *ConversationController) UpdateContactInfo(c *gin.Context) {
  111. id, err := parseUintParam(c, "id")
  112. if err != nil {
  113. c.JSON(http.StatusBadRequest, gin.H{"error": "会话ID不合法"})
  114. return
  115. }
  116. var req updateContactRequest
  117. if err := c.ShouldBindJSON(&req); err != nil {
  118. c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
  119. return
  120. }
  121. if req.Email == nil && req.Phone == nil && req.Notes == nil {
  122. c.JSON(http.StatusBadRequest, gin.H{"error": "至少提供一个需要更新的字段"})
  123. return
  124. }
  125. result, err := cc.conversationService.UpdateConversationContact(service.UpdateConversationContactInput{
  126. ConversationID: uint(id),
  127. Email: req.Email,
  128. Phone: req.Phone,
  129. Notes: req.Notes,
  130. })
  131. if err != nil {
  132. if err == service.ErrConversationNotFound {
  133. c.JSON(http.StatusNotFound, gin.H{"error": "会话不存在"})
  134. } else {
  135. c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
  136. }
  137. return
  138. }
  139. c.JSON(http.StatusOK, gin.H{
  140. "email": result.Email,
  141. "phone": result.Phone,
  142. "notes": result.Notes,
  143. })
  144. }
  145. // ListConversations 返回当前活跃会话的列表。type=internal 时返回该客服的内部对话(知识库测试)。
  146. func (cc *ConversationController) ListConversations(c *gin.Context) {
  147. var userID uint
  148. if userIDStr := c.Query("user_id"); userIDStr != "" {
  149. if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil {
  150. userID = uint(parsed)
  151. }
  152. }
  153. conversationType := c.DefaultQuery("type", "visitor")
  154. var conversations []service.ConversationSummary
  155. var err error
  156. if conversationType == "internal" {
  157. if userID == 0 {
  158. c.JSON(http.StatusBadRequest, gin.H{"error": "内部对话列表需要 user_id"})
  159. return
  160. }
  161. conversations, err = cc.conversationService.ListInternalConversations(userID)
  162. } else {
  163. conversations, err = cc.conversationService.ListConversations(userID)
  164. }
  165. if err != nil {
  166. c.JSON(http.StatusInternalServerError, gin.H{"error": "查询对话列表失败"})
  167. return
  168. }
  169. items := make([]gin.H, 0, len(conversations))
  170. for _, conv := range conversations {
  171. item := gin.H{
  172. "id": conv.ID,
  173. "conversation_type": conv.ConversationType,
  174. "visitor_id": conv.VisitorID,
  175. "agent_id": conv.AgentID,
  176. "status": conv.Status,
  177. "chat_mode": conv.ChatMode,
  178. "created_at": formatTimeValue(conv.CreatedAt),
  179. "updated_at": formatTimeValue(conv.UpdatedAt),
  180. "unread_count": conv.UnreadCount,
  181. "has_participated": conv.HasParticipated,
  182. }
  183. // 添加 last_seen_at 字段(用于判断在线状态)
  184. if lastSeen := formatTimePointer(conv.LastSeenAt); lastSeen != "" {
  185. item["last_seen_at"] = lastSeen
  186. }
  187. if conv.LastMessage != nil {
  188. item["last_message"] = gin.H{
  189. "id": conv.LastMessage.ID,
  190. "content": conv.LastMessage.Content,
  191. "sender_is_agent": conv.LastMessage.SenderIsAgent,
  192. "message_type": conv.LastMessage.MessageType,
  193. "is_read": conv.LastMessage.IsRead,
  194. "read_at": formatTimePointer(conv.LastMessage.ReadAt),
  195. "created_at": formatTimeValue(conv.LastMessage.CreatedAt),
  196. }
  197. }
  198. items = append(items, item)
  199. }
  200. c.JSON(http.StatusOK, items)
  201. }
  202. // GetConversationDetail 返回会话的详细信息。
  203. func (cc *ConversationController) GetConversationDetail(c *gin.Context) {
  204. id, err := parseUintParam(c, "id")
  205. if err != nil {
  206. c.JSON(http.StatusBadRequest, gin.H{"error": "会话ID不合法"})
  207. return
  208. }
  209. // 从查询参数获取 user_id(可选,用于检查参与状态)
  210. var userID uint
  211. if userIDStr := c.Query("user_id"); userIDStr != "" {
  212. // 使用 strconv 解析查询参数(不是路径参数)
  213. if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil {
  214. userID = uint(parsed)
  215. }
  216. }
  217. detail, err := cc.conversationService.GetConversationDetail(uint(id), userID)
  218. if err != nil {
  219. if err == service.ErrConversationNotFound {
  220. c.JSON(http.StatusNotFound, gin.H{"error": "会话不存在"})
  221. } else {
  222. c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
  223. }
  224. return
  225. }
  226. response := gin.H{
  227. "id": detail.ID,
  228. "visitor_id": detail.VisitorID,
  229. "agent_id": detail.AgentID,
  230. "status": detail.Status,
  231. "website": detail.Website,
  232. "referrer": detail.Referrer,
  233. "browser": detail.Browser,
  234. "os": detail.OS,
  235. "language": detail.Language,
  236. "ip_address": detail.IPAddress,
  237. "location": detail.Location,
  238. "email": detail.Email,
  239. "phone": detail.Phone,
  240. "notes": detail.Notes,
  241. "created_at": formatTimeValue(detail.CreatedAt),
  242. "updated_at": formatTimeValue(detail.UpdatedAt),
  243. "unread_count": detail.UnreadCount,
  244. }
  245. if lastSeen := formatTimePointer(detail.LastSeen); lastSeen != "" {
  246. response["last_seen_at"] = lastSeen
  247. }
  248. if detail.LastMessage != nil {
  249. response["last_message"] = gin.H{
  250. "id": detail.LastMessage.ID,
  251. "content": detail.LastMessage.Content,
  252. "sender_is_agent": detail.LastMessage.SenderIsAgent,
  253. "message_type": detail.LastMessage.MessageType,
  254. "is_read": detail.LastMessage.IsRead,
  255. "read_at": formatTimePointer(detail.LastMessage.ReadAt),
  256. "created_at": formatTimeValue(detail.LastMessage.CreatedAt),
  257. }
  258. }
  259. c.JSON(http.StatusOK, response)
  260. }
  261. // SearchConversations 根据关键字进行会话的模糊搜索。
  262. func (cc *ConversationController) SearchConversations(c *gin.Context) {
  263. query := c.Query("q")
  264. if query == "" {
  265. c.JSON(http.StatusBadRequest, gin.H{"error": "搜索关键词不能为空"})
  266. return
  267. }
  268. // 从查询参数获取 user_id(可选,用于检查参与状态)
  269. var userID uint
  270. if userIDStr := c.Query("user_id"); userIDStr != "" {
  271. // 使用 strconv 解析查询参数(不是路径参数)
  272. if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil {
  273. userID = uint(parsed)
  274. }
  275. }
  276. conversations, err := cc.conversationService.SearchConversations(query, userID)
  277. if err != nil {
  278. c.JSON(http.StatusInternalServerError, gin.H{"error": "搜索失败"})
  279. return
  280. }
  281. items := make([]gin.H, 0, len(conversations))
  282. for _, conv := range conversations {
  283. item := gin.H{
  284. "id": conv.ID,
  285. "visitor_id": conv.VisitorID,
  286. "agent_id": conv.AgentID,
  287. "status": conv.Status,
  288. "created_at": formatTimeValue(conv.CreatedAt),
  289. "updated_at": formatTimeValue(conv.UpdatedAt),
  290. "unread_count": conv.UnreadCount,
  291. "has_participated": conv.HasParticipated, // 当前用户是否参与过该会话
  292. }
  293. // 添加 last_seen_at 字段(用于判断在线状态)
  294. if lastSeen := formatTimePointer(conv.LastSeenAt); lastSeen != "" {
  295. item["last_seen_at"] = lastSeen
  296. }
  297. if conv.LastMessage != nil {
  298. item["last_message"] = gin.H{
  299. "id": conv.LastMessage.ID,
  300. "content": conv.LastMessage.Content,
  301. "sender_is_agent": conv.LastMessage.SenderIsAgent,
  302. "message_type": conv.LastMessage.MessageType,
  303. "is_read": conv.LastMessage.IsRead,
  304. "read_at": formatTimePointer(conv.LastMessage.ReadAt),
  305. "created_at": formatTimeValue(conv.LastMessage.CreatedAt),
  306. }
  307. }
  308. items = append(items, item)
  309. }
  310. c.JSON(http.StatusOK, items)
  311. }