refactor: remove ExternalSource and ExternalID from Conversation model and related services, update references to use ChannelID instead
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 标签搜索
|
||||
|
||||
+22
-24
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "-"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -339,8 +339,8 @@ export function ConversationDetailDialog({
|
||||
value={currentConversation.currentAssigneeName || "-"}
|
||||
/>
|
||||
<InfoItem
|
||||
label="渠道类型"
|
||||
value={currentConversation.externalSource || "-"}
|
||||
label="渠道ID"
|
||||
value={`${currentConversation.channelId || "-"}`}
|
||||
/>
|
||||
<InfoItem
|
||||
label="客服未读"
|
||||
|
||||
@@ -726,7 +726,7 @@ export default function DashboardConversationsPage() {
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium">{item.subject || `会话 #${item.id}`}</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
渠道:{item.externalSource || "-"}
|
||||
渠道ID:{item.channelId || "-"}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-2 text-sm text-muted-foreground">
|
||||
{item.lastMessageSummary || "暂无最新消息摘要"}
|
||||
|
||||
@@ -168,10 +168,10 @@ function MissingCustomerEmpty({ conversation }: { conversation: AgentConversatio
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<SectionHeading>访客标识</SectionHeading>
|
||||
<SectionHeading>会话归属</SectionHeading>
|
||||
<div className="space-y-2">
|
||||
<DetailRow label="外部来源" value={conversation.externalSource} />
|
||||
<DetailRow label="外部标识" value={conversation.externalId} />
|
||||
<DetailRow label="渠道ID" value={conversation.channelId ? `${conversation.channelId}` : "-"} />
|
||||
<DetailRow label="客户ID" value={conversation.customerId ? `${conversation.customerId}` : "-"} />
|
||||
</div>
|
||||
</div>
|
||||
<CustomerLinkOrCreateDialog
|
||||
|
||||
@@ -101,12 +101,6 @@ export function ConversationList({ onAfterSelect }: ConversationListProps) {
|
||||
>
|
||||
{getEnumLabel(IMConversationStatusLabels, conversation.status)}
|
||||
</span>
|
||||
{conversation.externalSource ? (
|
||||
<>
|
||||
<span className="opacity-40">·</span>
|
||||
<span className="truncate">{conversation.externalSource}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -292,9 +292,7 @@ export default function ConversationsPage() {
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground sm:text-sm">
|
||||
<span>{conversation.externalSource}</span>
|
||||
<span className="text-muted-foreground/60"> / </span>
|
||||
<span>{conversation.externalId}</span>
|
||||
<span>渠道 #{conversation.channelId || "-"}</span>
|
||||
{conversation.customerId ? (
|
||||
<>
|
||||
<span className="text-muted-foreground/60"> / </span>
|
||||
|
||||
@@ -109,8 +109,6 @@ export type ConversationParticipant = {
|
||||
export type AdminConversation = {
|
||||
id: number
|
||||
channelId: number
|
||||
externalSource: string
|
||||
externalId: string
|
||||
subject: string
|
||||
status: number
|
||||
serviceMode: number
|
||||
|
||||
@@ -37,8 +37,6 @@ export type AgentConversation = {
|
||||
aiAgentId?: number
|
||||
channelId?: number
|
||||
customerId?: number
|
||||
externalSource: string
|
||||
externalId: string
|
||||
subject: string
|
||||
status: number
|
||||
serviceMode: number
|
||||
|
||||
@@ -34,8 +34,6 @@ export type ImConversationParticipant = {
|
||||
export type ImConversation = {
|
||||
id: number
|
||||
channelId: number
|
||||
externalSource: string
|
||||
externalId: string
|
||||
subject: string
|
||||
status: number
|
||||
serviceMode: number
|
||||
|
||||
Reference in New Issue
Block a user