From 2c5e6e595fdf435efd9471678be403db0adf86fd Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 23 Aug 2026 17:32:35 +0800 Subject: [PATCH] fix(shangwutong): initialize contact attributes on inbox creation --- backend/internal/service/inbox_service.go | 36 +++++++++ .../internal/service/inbox_service_test.go | 78 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/backend/internal/service/inbox_service.go b/backend/internal/service/inbox_service.go index 30643e6b..a2b74bf2 100644 --- a/backend/internal/service/inbox_service.go +++ b/backend/internal/service/inbox_service.go @@ -23,6 +23,7 @@ import ( applogger "github.com/gochat/gochat/pkg/logger" pkgvalidator "github.com/gochat/gochat/pkg/validator" "gorm.io/gorm" + "gorm.io/gorm/clause" ) const InboxLimitExceededMessage = "Account limit exceeded. Upgrade to a higher plan" @@ -362,6 +363,38 @@ func (s *InboxService) syncAPIChannel(ctx context.Context, inbox *model.Inbox) e var shangwutongSessionIDPattern = regexp.MustCompile(`^[A-Za-z0-9]{11}$`) +var shangwutongContactAttributeDefinitions = []struct { + name, displayName, description string +}{ + {"swt_ip", "访客 IP", "商务通访客 IP 地址"}, + {"swt_ip_location", "IP 归属地", "商务通解析的访客 IP 归属地"}, + {"swt_resolution", "屏幕分辨率", "商务通访客设备屏幕分辨率"}, + {"swt_language", "浏览器语言", "商务通访客浏览器语言"}, + {"swt_timezone", "访客时区", "商务通访客设备时区"}, + {"swt_os", "操作系统", "商务通访客设备操作系统"}, + {"swt_browser", "浏览器", "商务通访客浏览器名称"}, + {"swt_browser_version", "浏览器版本", "商务通访客浏览器版本"}, + {"swt_user_agent", "User Agent", "商务通访客浏览器 User Agent"}, + {"swt_device", "访客设备", "商务通访客设备信息"}, + {"swt_query_title", "搜索主题", "商务通访客进入页面时的搜索主题"}, + {"swt_query_word", "搜索关键词", "商务通访客进入页面时的搜索关键词"}, + {"swt_traffic_source", "流量来源", "商务通访客流量来源"}, + {"swt_profile_channel", "来源渠道", "商务通访客来源渠道"}, + {"swt_site_id", "商务通站点 ID", "商务通站点标识"}, + {"swt_environment_version", "环境协议版本", "商务通 kind=7 环境信息格式版本;不是网络运营商"}, +} + +func ensureShangwutongContactAttributeDefinitions(tx *gorm.DB, accountID uint) error { + definitions := make([]model.CustomAttributeDefinition, len(shangwutongContactAttributeDefinitions)) + for index, definition := range shangwutongContactAttributeDefinitions { + definitions[index] = model.CustomAttributeDefinition{ + AccountID: accountID, AttributeName: definition.name, AttributeDisplayName: definition.displayName, + AttributeType: "text", AttributeModel: "contact_attribute", Description: definition.description, + } + } + return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&definitions).Error +} + func (s *InboxService) createShangwutongInbox(ctx context.Context, accountID uint, req CreateInboxRequest) (*model.Inbox, error) { if err := validateShangwutongChannelKeys(req.Channel); err != nil { return nil, err @@ -399,6 +432,9 @@ func (s *InboxService) createShangwutongInbox(ctx context.Context, accountID uin if err := tx.Create(inbox).Error; err != nil { return err } + if err := ensureShangwutongContactAttributeDefinitions(tx, accountID); err != nil { + return err + } channelAPI.InboxID = inbox.ID config.InboxID = inbox.ID if err := tx.Create(channelAPI).Error; err != nil { diff --git a/backend/internal/service/inbox_service_test.go b/backend/internal/service/inbox_service_test.go index d1331f7e..9e66dd6b 100644 --- a/backend/internal/service/inbox_service_test.go +++ b/backend/internal/service/inbox_service_test.go @@ -36,6 +36,7 @@ func setupInboxServiceTest(t *testing.T) (*InboxService, *gorm.DB) { &model.Account{}, &model.AgentBot{}, &model.Inbox{}, + &model.CustomAttributeDefinition{}, &model.AgentBotInbox{}, &model.WebhookSubscription{}, &model.BackgroundJob{}, @@ -346,6 +347,83 @@ func TestInboxService_CreateShangwutongPersistsSecretsOutsideInboxConfig(t *test assert.Equal(t, int64(1), config.ConfigVersion) } +func TestInboxService_CreateShangwutongEnsuresContactAttributeDefinitions(t *testing.T) { + svc, db := setupInboxServiceTest(t) + account := &model.Account{Name: "SWT attributes", Locale: "en", Active: true} + require.NoError(t, db.Create(account).Error) + require.NoError(t, db.Create(&model.CustomAttributeDefinition{ + AccountID: account.ID, AttributeName: "swt_ip", AttributeDisplayName: "保留现有名称", + AttributeType: "number", AttributeModel: "contact_attribute", Description: "保留现有说明", + }).Error) + + create := func(name, sessionID, username string) { + t.Helper() + _, err := svc.Create(context.Background(), account.ID, CreateInboxRequest{ + Name: name, ChannelType: "shangwutong", Channel: map[string]any{ + "session_id": sessionID, "username": username, "password": "secret", + "desired_presence": "online", "webhook_url": "http://connector:9100/webhooks/gochat/v1", + }, + }) + require.NoError(t, err) + } + create("商务通一", "BYT99917999", "agent1") + create("商务通二", "BYT99917998", "agent2") + + var definitions []model.CustomAttributeDefinition + require.NoError(t, db.Where("account_id = ? AND attribute_model = ? AND deleted_at IS NULL", account.ID, "contact_attribute").Order("attribute_name").Find(&definitions).Error) + require.Len(t, definitions, 16) + + want := []string{ + "swt_browser", "swt_browser_version", "swt_device", "swt_environment_version", + "swt_ip", "swt_ip_location", "swt_language", "swt_os", "swt_profile_channel", + "swt_query_title", "swt_query_word", "swt_resolution", "swt_site_id", + "swt_timezone", "swt_traffic_source", "swt_user_agent", + } + for index, definition := range definitions { + require.Equal(t, want[index], definition.AttributeName) + } + require.Equal(t, "保留现有名称", definitions[4].AttributeDisplayName) + require.Equal(t, "number", definitions[4].AttributeType) + require.Equal(t, "保留现有说明", definitions[4].Description) + + var legacySourceDefinitions int64 + require.NoError(t, db.Model(&model.CustomAttributeDefinition{}). + Where("account_id = ? AND attribute_name LIKE ?", account.ID, "swt_source_%"). + Count(&legacySourceDefinitions).Error) + require.Zero(t, legacySourceDefinitions) +} + +func TestInboxService_CreateShangwutongRollsBackWhenAttributeInitializationFails(t *testing.T) { + svc, db := setupInboxServiceTest(t) + account := &model.Account{Name: "SWT attribute rollback", Locale: "en", Active: true} + require.NoError(t, db.Create(account).Error) + require.NoError(t, db.Exec(` + CREATE TRIGGER fail_swt_attribute_definition + BEFORE INSERT ON custom_attribute_definitions + WHEN NEW.attribute_name = 'swt_ip_location' + BEGIN + SELECT RAISE(FAIL, 'forced attribute initialization failure'); + END; + `).Error) + + inbox, err := svc.Create(context.Background(), account.ID, CreateInboxRequest{ + Name: "商务通回滚", ChannelType: "shangwutong", Channel: map[string]any{ + "session_id": "BYT99917999", "username": "agent", "password": "secret", + "desired_presence": "online", "webhook_url": "http://connector:9100/webhooks/gochat/v1", + }, + }) + require.ErrorContains(t, err, "forced attribute initialization failure") + require.Nil(t, inbox) + + for _, target := range []any{ + &model.Inbox{}, &channelmodel.ChannelAPI{}, &model.ChannelShangwutongConfig{}, &model.CustomAttributeDefinition{}, + } { + var count int64 + require.NoError(t, db.Model(target).Count(&count).Error) + require.Zero(t, count) + } +} + func TestInboxService_ShangwutongLifecycleJobsAreTransactionalAndKeepRotationDeleteSnapshots(t *testing.T) { svc, db := setupInboxServiceTest(t) wp := worker.NewWorkerPool(db)