diff --git a/internal/builders/conversation_builder.go b/internal/builders/conversation_builder.go index 637b33a..667f4b4 100644 --- a/internal/builders/conversation_builder.go +++ b/internal/builders/conversation_builder.go @@ -19,8 +19,6 @@ func BuildConversation(item *models.Conversation) response.ConversationResponse AIAgentID: item.AIAgentID, ChannelID: item.ChannelID, CustomerID: item.CustomerID, - ExternalSource: item.ExternalSource, - ExternalID: item.ExternalID, Subject: item.Subject, Status: item.Status, ServiceMode: item.ServiceMode, @@ -38,11 +36,13 @@ func BuildConversation(item *models.Conversation) response.ConversationResponse AgentLastReadMessageID: readStateMessageID(agentReadState), AgentLastReadSeqNo: readStateSeqNo(agentReadState), AgentLastReadAt: readStateAt(agentReadState), - CustomerOnline: services.WsService.IsGuestOnline(item.ExternalID), ClosedAt: utils.FormatTimePtr(item.ClosedAt), ClosedBy: item.ClosedBy, CloseReason: item.CloseReason, } + if identity := services.ConversationService.GetConversationExternalIdentity(item); identity != nil { + ret.CustomerOnline = services.WsService.IsGuestOnline(identity.ExternalID) + } if item.CurrentAssigneeID > 0 { if user := services.UserService.Get(item.CurrentAssigneeID); user != nil { ret.CurrentAssigneeName = user.Nickname diff --git a/internal/controllers/dashboard/conversation_controller.go b/internal/controllers/dashboard/conversation_controller.go index e5e0289..67b307e 100644 --- a/internal/controllers/dashboard/conversation_controller.go +++ b/internal/controllers/dashboard/conversation_controller.go @@ -28,7 +28,6 @@ func (c *ConversationController) AnyList() *web.JsonResult { cnd := params.NewPagedSqlCnd(c.Ctx, params.QueryFilter{ParamName: "status"}, - params.QueryFilter{ParamName: "externalSource"}, params.QueryFilter{ParamName: "serviceMode"}, params.QueryFilter{ParamName: "currentAssigneeId"}, ).Desc("last_message_at").Desc("id") @@ -37,7 +36,7 @@ func (c *ConversationController) AnyList() *web.JsonResult { if keyword, _ := params.Get(c.Ctx, "keyword"); strs.IsNotBlank(keyword) { keywordLike := "%" + strings.TrimSpace(keyword) + "%" - cnd.Where("subject LIKE ? OR external_id LIKE ? OR last_message_summary LIKE ?", keywordLike, keywordLike, keywordLike) + cnd.Where("subject LIKE ? OR last_message_summary LIKE ?", keywordLike, keywordLike) } // 标签搜索 diff --git a/internal/models/models.go b/internal/models/models.go index 79e6aa9..cf29209 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -323,30 +323,28 @@ type Tag struct { // Conversation 客服会话。 type Conversation struct { - ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为会话主键。 - AIAgentID int64 `gorm:"type:bigint;not null;default:0;index"` // AIAgentID 为当前会话绑定的 AI Agent ID。 - ChannelID int64 `gorm:"type:bigint;not null;default:0;index"` // ChannelID 为该会话来源接入渠道ID。 - CustomerID int64 `gorm:"type:bigint;not null;default:0;index"` // CustomerID 为已关联的 CRM 客户 ID;0 表示未关联(访客仅 ExternalUserID)。 - ExternalSource enums.ExternalSource `gorm:"type:varchar(50);not null;default:'';index"` // ExternalSource 为外部身份来源。 - ExternalID string `gorm:"type:varchar(128);not null;default:'';index"` // ExternalID 为外部访客ID。 - Subject string `gorm:"type:varchar(255);not null;default:''"` // Subject 为会话标题或摘要。 - Status enums.IMConversationStatus `gorm:"type:int;not null;default:1;index"` // Status 为会话状态,如待接入、处理中、已关闭。 - ServiceMode enums.IMConversationServiceMode `gorm:"type:int;not null;default:3;index"` // ServiceMode 为服务模式,如仅AI、仅人工、AI优先人工接管。 - Priority int `gorm:"type:int;not null;default:0;index"` // Priority 为会话优先级。 - CurrentAssigneeID int64 `gorm:"type:bigint;not null;default:0;index"` // CurrentAssigneeID 为当前接待客服ID。 - CurrentTeamID int64 `gorm:"type:bigint;not null;default:0;index"` // CurrentTeamID 为当前处理客服组ID。 - LastMessageID int64 `gorm:"type:bigint;not null;default:0;index"` // LastMessageID 为最后一条消息ID。 - LastMessageAt time.Time `gorm:"type:datetime;index"` // LastMessageAt 为最后消息时间。 - LastActiveAt time.Time `gorm:"type:datetime;index"` // LastActiveAt 为会话最近活跃时间。 - LastMessageSummary string `gorm:"type:varchar(255);not null;default:''"` // LastMessageSummary 为最后一条消息摘要。 - CustomerUnreadCount int `gorm:"type:int;not null;default:0"` // CustomerUnreadCount 为用户侧未读数。 - AgentUnreadCount int `gorm:"type:int;not null;default:0"` // AgentUnreadCount 为客服侧未读数。 - HandoffAt *time.Time `gorm:"type:datetime;index"` // HandoffAt 为最近一次转人工时间。 - HandoffReason string `gorm:"type:varchar(255);not null;default:''"` // HandoffReason 为最近一次转人工原因。 - AIReplyRounds int `gorm:"type:int;not null;default:0"` // AIReplyRounds 为当前会话内 AI 已成功回复次数。 - ClosedAt *time.Time `gorm:"type:datetime;index"` // ClosedAt 为会话关闭时间。 - ClosedBy int64 `gorm:"type:bigint;not null;default:0;index"` // ClosedBy 为关闭人用户ID,访客关闭时写0。 - CloseReason string `gorm:"type:varchar(255);not null;default:''"` // CloseReason 为关闭原因。 + ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为会话主键。 + AIAgentID int64 `gorm:"type:bigint;not null;default:0;index"` // AIAgentID 为当前会话绑定的 AI Agent ID。 + ChannelID int64 `gorm:"type:bigint;not null;default:0;index"` // ChannelID 为该会话来源接入渠道ID。 + CustomerID int64 `gorm:"type:bigint;not null;default:0;index"` // CustomerID 为会话所属客户 ID。 + Subject string `gorm:"type:varchar(255);not null;default:''"` // Subject 为会话标题或摘要。 + Status enums.IMConversationStatus `gorm:"type:int;not null;default:1;index"` // Status 为会话状态,如待接入、处理中、已关闭。 + ServiceMode enums.IMConversationServiceMode `gorm:"type:int;not null;default:3;index"` // ServiceMode 为服务模式,如仅AI、仅人工、AI优先人工接管。 + Priority int `gorm:"type:int;not null;default:0;index"` // Priority 为会话优先级。 + CurrentAssigneeID int64 `gorm:"type:bigint;not null;default:0;index"` // CurrentAssigneeID 为当前接待客服ID。 + CurrentTeamID int64 `gorm:"type:bigint;not null;default:0;index"` // CurrentTeamID 为当前处理客服组ID。 + LastMessageID int64 `gorm:"type:bigint;not null;default:0;index"` // LastMessageID 为最后一条消息ID。 + LastMessageAt time.Time `gorm:"type:datetime;index"` // LastMessageAt 为最后消息时间。 + LastActiveAt time.Time `gorm:"type:datetime;index"` // LastActiveAt 为会话最近活跃时间。 + LastMessageSummary string `gorm:"type:varchar(255);not null;default:''"` // LastMessageSummary 为最后一条消息摘要。 + CustomerUnreadCount int `gorm:"type:int;not null;default:0"` // CustomerUnreadCount 为用户侧未读数。 + AgentUnreadCount int `gorm:"type:int;not null;default:0"` // AgentUnreadCount 为客服侧未读数。 + HandoffAt *time.Time `gorm:"type:datetime;index"` // HandoffAt 为最近一次转人工时间。 + HandoffReason string `gorm:"type:varchar(255);not null;default:''"` // HandoffReason 为最近一次转人工原因。 + AIReplyRounds int `gorm:"type:int;not null;default:0"` // AIReplyRounds 为当前会话内 AI 已成功回复次数。 + ClosedAt *time.Time `gorm:"type:datetime;index"` // ClosedAt 为会话关闭时间。 + ClosedBy int64 `gorm:"type:bigint;not null;default:0;index"` // ClosedBy 为关闭人用户ID,访客关闭时写0。 + CloseReason string `gorm:"type:varchar(255);not null;default:''"` // CloseReason 为关闭原因。 AuditFields } diff --git a/internal/pkg/dto/request/conversation_request.go b/internal/pkg/dto/request/conversation_request.go index 41b5601..cd2d1a4 100644 --- a/internal/pkg/dto/request/conversation_request.go +++ b/internal/pkg/dto/request/conversation_request.go @@ -12,7 +12,6 @@ const ( type ConversationListRequest struct { Status int `json:"status"` - ExternalSource string `json:"externalSource"` ServiceMode int `json:"serviceMode"` CurrentAssigneeID int64 `json:"currentAssigneeId"` Keyword string `json:"keyword"` diff --git a/internal/pkg/dto/response/conversation_response.go b/internal/pkg/dto/response/conversation_response.go index e0d34d1..ed315ec 100644 --- a/internal/pkg/dto/response/conversation_response.go +++ b/internal/pkg/dto/response/conversation_response.go @@ -22,8 +22,6 @@ type ConversationResponse struct { AIAgentID int64 `json:"aiAgentId"` ChannelID int64 `json:"channelId"` CustomerID int64 `json:"customerId"` - ExternalSource enums.ExternalSource `json:"externalSource"` - ExternalID string `json:"externalId"` Subject string `json:"subject"` Status enums.IMConversationStatus `json:"status"` ServiceMode enums.IMConversationServiceMode `json:"serviceMode"` diff --git a/internal/repositories/customer_identity_repository.go b/internal/repositories/customer_identity_repository.go index a616fca..a911738 100644 --- a/internal/repositories/customer_identity_repository.go +++ b/internal/repositories/customer_identity_repository.go @@ -45,6 +45,13 @@ func (r *customerIdentityRepository) GetBy(db *gorm.DB, externalSource enums.Ext Eq("external_id", externalID)) } +func (r *customerIdentityRepository) FindByCustomerID(db *gorm.DB, customerID int64) []models.CustomerIdentity { + if customerID <= 0 { + return nil + } + return r.Find(db, sqls.NewCnd().Eq("customer_id", customerID).Eq("status", enums.StatusOk).Desc("id")) +} + func (r *customerIdentityRepository) Find(db *gorm.DB, cnd *sqls.Cnd) (list []models.CustomerIdentity) { cnd.Find(db, &list) return diff --git a/internal/services/channel_message_outbox_service.go b/internal/services/channel_message_outbox_service.go index 2442b6e..a94ba68 100644 --- a/internal/services/channel_message_outbox_service.go +++ b/internal/services/channel_message_outbox_service.go @@ -78,7 +78,8 @@ func (s *channelMessageOutboxService) EnqueueWxWorkKFMessage(conversation *model if conversation == nil || message == nil { return nil } - if conversation.ExternalSource != enums.ExternalSourceWxWorkKF { + channel := ChannelService.Get(conversation.ChannelID) + if channel == nil || channel.ChannelType != enums.ChannelTypeWxWorkKF { return nil } if message.SenderType != enums.IMSenderTypeAgent && message.SenderType != enums.IMSenderTypeAI { diff --git a/internal/services/conversation_service.go b/internal/services/conversation_service.go index 888ab21..fd5f03e 100644 --- a/internal/services/conversation_service.go +++ b/internal/services/conversation_service.go @@ -25,6 +25,7 @@ import ( "github.com/mlogclub/simple/common/strs" "github.com/mlogclub/simple/sqls" + "gorm.io/gorm" ) var ConversationService = newConversationService() @@ -60,7 +61,7 @@ func (s *conversationService) ListConversations(userID int64, filter request.Age if strs.IsNotBlank(keyword) { keyword = strings.TrimSpace(keyword) - cnd.Where("subject LIKE ? OR external_id LIKE ? OR last_message_summary LIKE ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") + cnd.Where("subject LIKE ? OR last_message_summary LIKE ?", "%"+keyword+"%", "%"+keyword+"%") } switch filter { @@ -86,51 +87,56 @@ func (s *conversationService) Updates(id int64, columns map[string]interface{}) return repositories.ConversationRepository.Updates(sqls.DB(), id, columns) } -func (s *conversationService) getLatestNotFinished(externalInfo openidentity.ExternalInfo) *models.Conversation { +func (s *conversationService) getLatestNotFinishedByCustomerID(db *gorm.DB, customerID int64) *models.Conversation { + if customerID <= 0 { + return nil + } cnd := sqls.NewCnd() - cnd.Eq("external_id", externalInfo.ExternalID) - cnd.Eq("external_source", externalInfo.ExternalSource) + cnd.Eq("customer_id", customerID) cnd.In("status", []enums.IMConversationStatus{ enums.IMConversationStatusAIServing, enums.IMConversationStatusPending, enums.IMConversationStatusActive, }) cnd.Desc("id") - return s.FindOne(cnd) + return repositories.ConversationRepository.FindOne(db, cnd) } func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, channelID, aiAgentID int64) (*models.Conversation, error) { subject := s.buildDefaultSubject(externalInfo) - // 会话存在,直接返回 - if conversation := s.getLatestNotFinished(externalInfo); conversation != nil { - return conversation, nil - } - aiAgent := AIAgentService.Get(aiAgentID) if aiAgent == nil || aiAgent.Status != enums.StatusOk { return nil, errorsx.InvalidParam("AI Agent not found") } - conversation := &models.Conversation{ - AIAgentID: aiAgentID, - ChannelID: channelID, - ExternalSource: externalInfo.ExternalSource, - Subject: subject, - Status: s.resolveInitialStatus(aiAgent.ServiceMode), - ServiceMode: aiAgent.ServiceMode, - Priority: 0, - ExternalID: externalInfo.ExternalID, - CurrentAssigneeID: 0, - CurrentTeamID: 0, - LastMessageAt: time.Now(), - LastActiveAt: time.Now(), - AuditFields: utils.BuildAuditFields(nil), - } - if identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), externalInfo.ExternalSource, externalInfo.ExternalID); identity != nil { - conversation.CustomerID = identity.CustomerID - } + var conversation *models.Conversation + created := false if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { + customerID, err := s.ensureExternalCustomer(ctx.Tx, externalInfo) + if err != nil { + return err + } + if existing := s.getLatestNotFinishedByCustomerID(ctx.Tx, customerID); existing != nil { + conversation = existing + return nil + } + created = true + now := time.Now() + conversation = &models.Conversation{ + AIAgentID: aiAgentID, + ChannelID: channelID, + CustomerID: customerID, + Subject: subject, + Status: s.resolveInitialStatus(aiAgent.ServiceMode), + ServiceMode: aiAgent.ServiceMode, + Priority: 0, + CurrentAssigneeID: 0, + CurrentTeamID: 0, + LastMessageAt: now, + LastActiveAt: now, + AuditFields: utils.BuildAuditFields(nil), + } if err := ctx.Tx.Create(conversation).Error; err != nil { return err } @@ -141,6 +147,12 @@ func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, cha }); err != nil { return nil, err } + if conversation == nil { + return nil, errorsx.BusinessError(1, "创建会话失败") + } + if !created { + return conversation, nil + } // 推送会话创建事件 WsService.PublishConversationChanged(conversation, enums.IMRealtimeEventConversationCreated) @@ -158,6 +170,43 @@ func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, cha return s.Get(conversation.ID), nil } +func (s *conversationService) ensureExternalCustomer(db *gorm.DB, externalInfo openidentity.ExternalInfo) (int64, error) { + externalSource := externalInfo.ExternalSource + externalID := strings.TrimSpace(externalInfo.ExternalID) + if strings.TrimSpace(string(externalSource)) == "" || externalID == "" { + return 0, errorsx.Unauthorized("外部用户标识不能为空") + } + now := time.Now() + if identity := repositories.CustomerIdentityRepository.GetBy(db, externalSource, externalID); identity != nil { + _ = repositories.CustomerRepository.Updates(db, identity.CustomerID, map[string]any{ + "last_active_at": now, + "updated_at": now, + }) + return identity.CustomerID, nil + } + + customer := &models.Customer{ + Name: s.buildDefaultSubject(externalInfo), + LastActiveAt: &now, + Status: enums.StatusOk, + AuditFields: utils.BuildAuditFields(nil), + } + if err := repositories.CustomerRepository.Create(db, customer); err != nil { + return 0, err + } + identity := &models.CustomerIdentity{ + CustomerID: customer.ID, + ExternalSource: externalSource, + ExternalID: externalID, + Status: enums.StatusOk, + AuditFields: utils.BuildAuditFields(nil), + } + if err := repositories.CustomerIdentityRepository.Create(db, identity); err != nil { + return 0, err + } + return customer.ID, nil +} + func (s *conversationService) AssignConversation(req request.AssignConversationRequest, operator *dto.AuthPrincipal) error { if operator == nil { return errorsx.Unauthorized("未登录或登录已过期") @@ -638,20 +687,14 @@ func (s *conversationService) IsCustomerConversationOwner(conversation *models.C return false } extID := strings.TrimSpace(externalInfo.ExternalID) - if extID == "" || strings.TrimSpace(conversation.ExternalID) == "" { + if extID == "" || strings.TrimSpace(string(externalInfo.ExternalSource)) == "" || conversation.CustomerID <= 0 { return false } - if conversation.ExternalID != extID { + identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), externalInfo.ExternalSource, extID) + if identity == nil { return false } - reqSrc := strings.TrimSpace(string(externalInfo.ExternalSource)) - convSrc := strings.TrimSpace(string(conversation.ExternalSource)) - if convSrc != "" { - if reqSrc == "" || reqSrc != convSrc { - return false - } - } - return true + return identity.CustomerID == conversation.CustomerID } func (s *conversationService) BuildConversationSummary(conversation *models.Conversation) string { @@ -704,7 +747,7 @@ func (s *conversationService) buildEventPayload(payload map[string]any) string { return string(data) } -// LinkConversationCustomer 将会话绑定到指定客户;若会话带外部访客标识则维护 CustomerIdentity(与创建会话时逻辑一致)。 +// LinkConversationCustomer 将会话绑定到指定客户。 func (s *conversationService) LinkConversationCustomer(conversationID, customerID int64, operator *dto.AuthPrincipal) error { if operator == nil { return errorsx.Unauthorized("未登录或登录已过期") @@ -727,33 +770,11 @@ func (s *conversationService) LinkConversationCustomer(conversationID, customerI return errorsx.Forbidden("无权限关联该会话") } - extID := strings.TrimSpace(conv.ExternalID) - extSrc := strings.TrimSpace(string(conv.ExternalSource)) - err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { current := repositories.ConversationRepository.Get(ctx.Tx, conversationID) if current == nil { return errorsx.InvalidParam("会话不存在") } - if extID != "" && extSrc != "" { - existing := repositories.CustomerIdentityRepository.GetBy(ctx.Tx, enums.ExternalSource(extSrc), extID) - if existing != nil { - if existing.CustomerID != customerID { - return errorsx.BusinessError(1, "该访客身份已绑定其他客户,无法关联到当前选择") - } - } else { - idRow := &models.CustomerIdentity{ - CustomerID: customerID, - ExternalSource: enums.ExternalSource(extSrc), - ExternalID: extID, - Status: enums.StatusOk, - AuditFields: utils.BuildAuditFields(operator), - } - if err := repositories.CustomerIdentityRepository.Create(ctx.Tx, idRow); err != nil { - return err - } - } - } now := time.Now() return repositories.ConversationRepository.Updates(ctx.Tx, conversationID, map[string]any{ "customer_id": customerID, @@ -771,6 +792,38 @@ func (s *conversationService) LinkConversationCustomer(conversationID, customerI return nil } +func (s *conversationService) GetConversationExternalIdentity(conversation *models.Conversation) *models.CustomerIdentity { + if conversation == nil || conversation.CustomerID <= 0 { + return nil + } + identities := repositories.CustomerIdentityRepository.FindByCustomerID(sqls.DB(), conversation.CustomerID) + if len(identities) == 0 { + return nil + } + if channel := ChannelService.Get(conversation.ChannelID); channel != nil { + expected := externalSourceForChannelType(channel.ChannelType) + if strings.TrimSpace(string(expected)) != "" { + for i := range identities { + if identities[i].ExternalSource == expected { + return &identities[i] + } + } + } + } + return &identities[0] +} + +func externalSourceForChannelType(channelType string) enums.ExternalSource { + switch strings.TrimSpace(channelType) { + case enums.ChannelTypeWxWorkKF: + return enums.ExternalSourceWxWorkKF + case enums.ChannelTypeWeb: + return enums.ExternalSourceGuest + default: + return "" + } +} + func (s *conversationService) canLinkConversationCustomer(conv *models.Conversation, operator *dto.AuthPrincipal) bool { if conv == nil || operator == nil { return false diff --git a/internal/services/event_handlers/conversation_assigned_event_handler.go b/internal/services/event_handlers/conversation_assigned_event_handler.go index 2de43ea..65de9c8 100644 --- a/internal/services/event_handlers/conversation_assigned_event_handler.go +++ b/internal/services/event_handlers/conversation_assigned_event_handler.go @@ -56,7 +56,7 @@ func buildConversationAssignedNotifyBody(conversation *models.Conversation, assi lines := []string{ fmt.Sprintf("会话ID: #%d", conversation.ID), fmt.Sprintf("会话主题: %s", strs.DefaultIfBlank(conversation.Subject, "-")), - fmt.Sprintf("接入渠道: %s", enums.GetExternalSourceLabel(conversation.ExternalSource)), + fmt.Sprintf("接入渠道: %s", resolveConversationChannelLabel(conversation)), fmt.Sprintf("当前状态: %s", enums.GetIMConversationStatusLabel(conversation.Status)), fmt.Sprintf("处理人: %s", resolveNotifyUserLabel(assigneeID)), } @@ -67,6 +67,16 @@ func buildConversationAssignedNotifyBody(conversation *models.Conversation, assi return strings.Join(lines, "\n") } +func resolveConversationChannelLabel(conversation *models.Conversation) string { + if conversation == nil || conversation.ChannelID <= 0 { + return "-" + } + if channel := services.ChannelService.Get(conversation.ChannelID); channel != nil { + return strs.DefaultIfBlank(channel.Name, channel.ChannelType) + } + return "-" +} + func resolveNotifyUserLabel(userID int64) string { if userID <= 0 { return "-" diff --git a/internal/services/message_service.go b/internal/services/message_service.go index 476ee08..c31fe94 100644 --- a/internal/services/message_service.go +++ b/internal/services/message_service.go @@ -410,7 +410,6 @@ func (s *messageService) sendMessage(conversationID int64, senderType enums.IMSe slog.Error("enqueue wxwork kf outbox failed", "conversation_id", conversation.ID, "message_id", message.ID, - "external_source", conversation.ExternalSource, "error", enqueueErr, ) } diff --git a/internal/services/ticket_service.go b/internal/services/ticket_service.go index df6fa40..87d78fe 100644 --- a/internal/services/ticket_service.go +++ b/internal/services/ticket_service.go @@ -862,7 +862,7 @@ func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConve Title: title, Description: description, Source: string(enums.TicketSourceConversation), - Channel: string(conversation.ExternalSource), + Channel: s.resolveConversationChannel(conversation), CustomerID: conversation.CustomerID, ConversationID: conversation.ID, TagIDs: req.TagIDs, @@ -1845,6 +1845,16 @@ func parseOptionalDateTime(value string) (*time.Time, error) { return nil, fmt.Errorf("invalid datetime") } +func (s *ticketService) resolveConversationChannel(conversation *models.Conversation) string { + if conversation == nil || conversation.ChannelID <= 0 { + return "" + } + if channel := ChannelService.Get(conversation.ChannelID); channel != nil { + return channel.ChannelType + } + return "" +} + func diffMinutes(start *time.Time, end time.Time) int { if start == nil || start.IsZero() { return 0 diff --git a/internal/services/ws_service.go b/internal/services/ws_service.go index c22d8a6..68be67e 100644 --- a/internal/services/ws_service.go +++ b/internal/services/ws_service.go @@ -514,8 +514,8 @@ func (s *wsService) routeConversationTopics(conversation *models.Conversation) [ } topics := []string{s.conversationTopic(conversation.ID)} - if strings.TrimSpace(conversation.ExternalID) != "" { - topics = append(topics, s.guestTopic(conversation.ExternalID)) + if identity := ConversationService.GetConversationExternalIdentity(conversation); identity != nil && strings.TrimSpace(identity.ExternalID) != "" { + topics = append(topics, s.guestTopic(identity.ExternalID)) } if conversation.CurrentAssigneeID > 0 { topics = append(topics, s.adminTopic(conversation.CurrentAssigneeID)) diff --git a/internal/services/wxwork_kf_inbound_service.go b/internal/services/wxwork_kf_inbound_service.go index aa8d560..9e36d72 100644 --- a/internal/services/wxwork_kf_inbound_service.go +++ b/internal/services/wxwork_kf_inbound_service.go @@ -13,7 +13,6 @@ import ( "cs-agent/internal/wxwork" "github.com/mlogclub/simple/common/strs" - "github.com/mlogclub/simple/sqls" "github.com/silenceper/wechat/v2/work/kf" "github.com/silenceper/wechat/v2/work/kf/syncmsg" ) @@ -378,21 +377,9 @@ func (s *wxWorkKFInboundService) ensureConversation(base syncmsg.BaseMessage, pr } external := s.buildExternalInfo(externalID) - conversation := ConversationService.FindOne(sqls.NewCnd(). - Eq("external_source", external.ExternalSource). - Eq("external_id", external.ExternalID). - In("status", []enums.IMConversationStatus{ - enums.IMConversationStatusAIServing, - enums.IMConversationStatusPending, - enums.IMConversationStatusActive, - }). - Desc("id")) - - if conversation == nil { - conversation, err = ConversationService.Create(external, channel.ID, channel.AIAgentID) - if err != nil { - return nil, err - } + conversation, err := ConversationService.Create(external, channel.ID, channel.AIAgentID) + if err != nil { + return nil, err } if err := s.upsertConversationMapping( diff --git a/web/app/dashboard/conversation-monitor/_components/detail.tsx b/web/app/dashboard/conversation-monitor/_components/detail.tsx index 6890862..12d2aad 100644 --- a/web/app/dashboard/conversation-monitor/_components/detail.tsx +++ b/web/app/dashboard/conversation-monitor/_components/detail.tsx @@ -339,8 +339,8 @@ export function ConversationDetailDialog({ value={currentConversation.currentAssigneeName || "-"} />
{item.subject || `会话 #${item.id}`}
- 渠道:{item.externalSource || "-"} + 渠道ID:{item.channelId || "-"}
{item.lastMessageSummary || "暂无最新消息摘要"} diff --git a/web/app/dashboard/conversations/_components/conversation-info-panel.tsx b/web/app/dashboard/conversations/_components/conversation-info-panel.tsx index a900a90..3ba1553 100644 --- a/web/app/dashboard/conversations/_components/conversation-info-panel.tsx +++ b/web/app/dashboard/conversations/_components/conversation-info-panel.tsx @@ -168,10 +168,10 @@ function MissingCustomerEmpty({ conversation }: { conversation: AgentConversatio
- 访客标识 + 会话归属
- - + +
{getEnumLabel(IMConversationStatusLabels, conversation.status)} - {conversation.externalSource ? ( - <> - · - {conversation.externalSource} - - ) : null} diff --git a/web/app/dashboard/conversations/page.tsx b/web/app/dashboard/conversations/page.tsx index 42c4324..9ca3d15 100644 --- a/web/app/dashboard/conversations/page.tsx +++ b/web/app/dashboard/conversations/page.tsx @@ -292,9 +292,7 @@ export default function ConversationsPage() {

- {conversation.externalSource} - / - {conversation.externalId} + 渠道 #{conversation.channelId || "-"} {conversation.customerId ? ( <> / diff --git a/web/lib/api/admin.ts b/web/lib/api/admin.ts index be86cfe..64013fa 100644 --- a/web/lib/api/admin.ts +++ b/web/lib/api/admin.ts @@ -109,8 +109,6 @@ export type ConversationParticipant = { export type AdminConversation = { id: number channelId: number - externalSource: string - externalId: string subject: string status: number serviceMode: number diff --git a/web/lib/api/agent.ts b/web/lib/api/agent.ts index fe47e72..30b974a 100644 --- a/web/lib/api/agent.ts +++ b/web/lib/api/agent.ts @@ -37,8 +37,6 @@ export type AgentConversation = { aiAgentId?: number channelId?: number customerId?: number - externalSource: string - externalId: string subject: string status: number serviceMode: number diff --git a/web/lib/api/im.ts b/web/lib/api/im.ts index c0f08b5..9ddae7d 100644 --- a/web/lib/api/im.ts +++ b/web/lib/api/im.ts @@ -34,8 +34,6 @@ export type ImConversationParticipant = { export type ImConversation = { id: number channelId: number - externalSource: string - externalId: string subject: string status: number serviceMode: number