refactor: remove ExternalSource and ExternalID from Conversation model and related services, update references to use ChannelID instead

This commit is contained in:
mlogclub
2026-04-27 19:04:10 +08:00
parent 24105781d9
commit b3ef55b04e
21 changed files with 183 additions and 136 deletions
+114 -61
View File
@@ -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