diff --git a/internal/builders/conversation_builder.go b/internal/builders/conversation_builder.go index b4c722c..9663be2 100644 --- a/internal/builders/conversation_builder.go +++ b/internal/builders/conversation_builder.go @@ -19,7 +19,7 @@ func BuildConversation(item *models.Conversation) response.ConversationResponse AIAgentID: item.AIAgentID, ChannelID: item.ChannelID, CustomerID: item.CustomerID, - CustomerName: buildConversationCustomerName(item.CustomerID), + CustomerName: item.CustomerName, Status: item.Status, ServiceMode: item.ServiceMode, Priority: item.Priority, @@ -62,16 +62,6 @@ func BuildConversation(item *models.Conversation) response.ConversationResponse return ret } -func buildConversationCustomerName(customerID int64) string { - if customerID <= 0 { - return "" - } - if customer := services.CustomerService.Get(customerID); customer != nil { - return strings.TrimSpace(customer.Name) - } - return "" -} - func BuildParticipantResponses(conversationID int64) []response.ConversationParticipantResponse { list := services.ConversationParticipantService.Find(sqls.NewCnd().Eq("conversation_id", conversationID).Asc("id")) if len(list) == 0 { diff --git a/internal/controllers/dashboard/conversation_controller.go b/internal/controllers/dashboard/conversation_controller.go index e1be192..548038e 100644 --- a/internal/controllers/dashboard/conversation_controller.go +++ b/internal/controllers/dashboard/conversation_controller.go @@ -36,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("last_message_summary LIKE ? OR customer_id IN (SELECT id FROM t_customer WHERE name LIKE ?)", keywordLike, keywordLike) + cnd.Where("customer_name LIKE ? OR last_message_summary LIKE ?", keywordLike, keywordLike) } // 标签搜索 diff --git a/internal/migration/000006_backfill_conversation_customer_name.go b/internal/migration/000006_backfill_conversation_customer_name.go new file mode 100644 index 0000000..a98bfd4 --- /dev/null +++ b/internal/migration/000006_backfill_conversation_customer_name.go @@ -0,0 +1,24 @@ +package migration + +import ( + "cs-agent/internal/models" + + "github.com/mlogclub/simple/sqls" +) + +func init() { + register(6, "backfill conversation customer name", func() error { + db := sqls.DB() + if !db.Migrator().HasColumn(&models.Conversation{}, "customer_name") { + return nil + } + return db.Exec(` +UPDATE t_conversation +SET customer_name = ( + SELECT name FROM t_customer WHERE t_customer.id = t_conversation.customer_id +) +WHERE customer_id > 0 + AND (customer_name = '' OR customer_name IS NULL) +`).Error + }) +} diff --git a/internal/models/models.go b/internal/models/models.go index 80287b0..34fa96a 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -323,27 +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 为会话所属客户 ID。 - 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。 + CustomerName string `gorm:"type:varchar(100);not null;default:'';index"` // CustomerName 为客户名称冗余字段,用于列表展示和搜索。 + 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/repositories/conversation_repository.go b/internal/repositories/conversation_repository.go index 1ac7859..1bfa08e 100644 --- a/internal/repositories/conversation_repository.go +++ b/internal/repositories/conversation_repository.go @@ -91,6 +91,11 @@ func (r *conversationRepository) Updates(db *gorm.DB, id int64, columns map[stri return } +func (r *conversationRepository) UpdatesByCustomerID(db *gorm.DB, customerID int64, columns map[string]interface{}) (err error) { + err = db.Model(&models.Conversation{}).Where("customer_id = ?", customerID).Updates(columns).Error + return +} + func (r *conversationRepository) UpdateColumn(db *gorm.DB, id int64, name string, value interface{}) (err error) { err = db.Model(&models.Conversation{}).Where("id = ?", id).UpdateColumn(name, value).Error return diff --git a/internal/services/conversation_service.go b/internal/services/conversation_service.go index 8cf4309..54d5b82 100644 --- a/internal/services/conversation_service.go +++ b/internal/services/conversation_service.go @@ -59,7 +59,7 @@ func (s *conversationService) ListConversations(userID int64, filter request.Age if strs.IsNotBlank(keyword) { keyword = strings.TrimSpace(keyword) keywordLike := "%" + keyword + "%" - cnd.Where("last_message_summary LIKE ? OR customer_id IN (SELECT id FROM t_customer WHERE name LIKE ?)", keywordLike, keywordLike) + cnd.Where("customer_name LIKE ? OR last_message_summary LIKE ?", keywordLike, keywordLike) } switch filter { @@ -113,8 +113,18 @@ func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, cha if err != nil { return err } + customerName := s.getCustomerName(ctx.Tx, customerID) if existing := s.getLatestNotFinishedByCustomerID(ctx.Tx, customerID); existing != nil { conversation = existing + if customerName != "" && existing.CustomerName != customerName { + if err := repositories.ConversationRepository.Updates(ctx.Tx, existing.ID, map[string]any{ + "customer_name": customerName, + "updated_at": time.Now(), + }); err != nil { + return err + } + conversation.CustomerName = customerName + } return nil } created = true @@ -123,6 +133,7 @@ func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, cha AIAgentID: aiAgentID, ChannelID: channelID, CustomerID: customerID, + CustomerName: customerName, Status: s.resolveInitialStatus(aiAgent.ServiceMode), ServiceMode: aiAgent.ServiceMode, Priority: 0, @@ -662,7 +673,14 @@ func (s *conversationService) BuildConversationSummary(conversation *models.Conv if strings.TrimSpace(conversation.LastMessageSummary) != "" { return conversation.LastMessageSummary } - if customer := CustomerService.Get(conversation.CustomerID); customer != nil { + return strings.TrimSpace(conversation.CustomerName) +} + +func (s *conversationService) getCustomerName(db *gorm.DB, customerID int64) string { + if customerID <= 0 { + return "" + } + if customer := repositories.CustomerRepository.Get(db, customerID); customer != nil { return strings.TrimSpace(customer.Name) } return "" @@ -739,6 +757,7 @@ func (s *conversationService) LinkConversationCustomer(conversationID, customerI now := time.Now() return repositories.ConversationRepository.Updates(ctx.Tx, conversationID, map[string]any{ "customer_id": customerID, + "customer_name": strings.TrimSpace(cust.Name), "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": now, diff --git a/internal/services/customer_service.go b/internal/services/customer_service.go index 614f44b..9e4ec46 100644 --- a/internal/services/customer_service.go +++ b/internal/services/customer_service.go @@ -218,16 +218,22 @@ func (s *customerService) UpdateCustomer(req request.UpdateCustomerRequest, oper } } - return repositories.CustomerRepository.Updates(sqls.DB(), req.ID, map[string]any{ - "name": name, - "gender": req.Gender, - "company_id": req.CompanyID, - "primary_mobile": strings.TrimSpace(req.PrimaryMobile), - "primary_email": strings.TrimSpace(req.PrimaryEmail), - "remark": strings.TrimSpace(req.Remark), - "update_user_id": operator.UserID, - "update_user_name": operator.Username, - "updated_at": time.Now(), + return sqls.WithTransaction(func(ctx *sqls.TxContext) error { + now := time.Now() + if err := repositories.CustomerRepository.Updates(ctx.Tx, req.ID, map[string]any{ + "name": name, + "gender": req.Gender, + "company_id": req.CompanyID, + "primary_mobile": strings.TrimSpace(req.PrimaryMobile), + "primary_email": strings.TrimSpace(req.PrimaryEmail), + "remark": strings.TrimSpace(req.Remark), + "update_user_id": operator.UserID, + "update_user_name": operator.Username, + "updated_at": now, + }); err != nil { + return err + } + return s.syncConversationCustomerName(ctx.Tx, req.ID, name, operator, now) }) } @@ -244,6 +250,21 @@ func (s *customerService) DeleteCustomer(id int64, operator dto.AuthPrincipal) e }) } +func (s *customerService) syncConversationCustomerName(db *gorm.DB, customerID int64, name string, operator *dto.AuthPrincipal, now time.Time) error { + if customerID <= 0 { + return nil + } + updates := map[string]any{ + "customer_name": strings.TrimSpace(name), + "updated_at": now, + } + if operator != nil { + updates["update_user_id"] = operator.UserID + updates["update_user_name"] = operator.Username + } + return repositories.ConversationRepository.UpdatesByCustomerID(db, customerID, updates) +} + func (s *customerService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error { if operator == nil { return errorsx.Unauthorized("未登录或登录已过期") @@ -316,6 +337,9 @@ func (s *customerService) SaveCustomerProfile(req request.SaveCustomerProfileReq }); err != nil { return err } + if err := s.syncConversationCustomerName(ctx.Tx, customerID, name, operator, now); err != nil { + return err + } out = repositories.CustomerRepository.Get(ctx.Tx, customerID) } return CustomerContactService.ReplaceAllForCustomerInTx(ctx, customerID, req.Contacts, operator)