fix(shangwutong): initialize contact attributes on inbox creation

This commit is contained in:
2026-08-23 17:32:35 +08:00
parent 25ef9ae494
commit 2c5e6e595f
2 changed files with 114 additions and 0 deletions
+36
View File
@@ -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 {
@@ -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)