From 8bccda5649b49b509b9769b92dcf226e6802987f Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sat, 9 Aug 2025 13:07:57 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(db):=20allow=20re-adding=20m?= =?UTF-8?q?odels/vendors=20after=20soft=20delete=20via=20composite=20uniqu?= =?UTF-8?q?e=20indexes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure models and vendors can be re-created after soft deletion by switching to composite unique indexes on (name, deleted_at) and cleaning up legacy single-column unique indexes on MySQL. Why - MySQL raised 1062 duplicate key errors when re-adding a soft-deleted model/vendor because the legacy unique index enforced uniqueness on the name column alone (uk_model_name / uk_vendor_name), despite soft deletes. - Users encountered errors such as: - Error 1062 (23000): Duplicate entry 'deepseek-chat' for key 'models.uk_model_name' - Error 1062 (23000): Duplicate entry 'DeepSeek' for key 'vendors.uk_vendor_name' How - Model indices: - model/model_meta.go: - Model.ModelName → gorm: uniqueIndex:uk_model_name,priority:1 - Model.DeletedAt → gorm: index; uniqueIndex:uk_model_name,priority:2 - Vendor indices: - model/vendor_meta.go: - Vendor.Name → gorm: uniqueIndex:uk_vendor_name,priority:1 - Vendor.DeletedAt → gorm: index; uniqueIndex:uk_vendor_name,priority:2 - Migration (automatic, idempotent): - model/main.go (migrateDB, migrateDBFast): - On MySQL, drop legacy single-column unique indexes if present: - ALTER TABLE models DROP INDEX uk_model_name; - ALTER TABLE vendors DROP INDEX uk_vendor_name; - Then run AutoMigrate to create composite unique indexes. - Missing-index errors are ignored to keep the migration safe to run multiple times. Result - Users can delete and re-add the same model/vendor name without manual SQL. - Migration runs automatically at startup; no user action required. - PostgreSQL and SQLite remain unaffected. Files changed - model/model_meta.go - model/vendor_meta.go - model/main.go (migrateDB, migrateDBFast) Testing - Create model "deepseek-chat" → delete (soft) → re-create → succeeds. - Create vendor "DeepSeek" → delete (soft) → re-create → succeeds. Backward compatibility - Data remains intact; only index definitions are updated. - Behavior is unchanged except for fixing the uniqueness constraint with soft deletes. --- model/main.go | 12 ++++++++++++ model/model_meta.go | 4 ++-- model/vendor_meta.go | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/model/main.go b/model/main.go index b93f01a2..08e3553a 100644 --- a/model/main.go +++ b/model/main.go @@ -235,6 +235,12 @@ func InitLogDB() (err error) { } func migrateDB() error { + // 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录 + if common.UsingMySQL { + // 旧索引可能不存在,忽略删除错误即可 + _ = DB.Exec("ALTER TABLE models DROP INDEX uk_model_name;").Error + _ = DB.Exec("ALTER TABLE vendors DROP INDEX uk_vendor_name;").Error + } if !common.UsingPostgreSQL { return migrateDBFast() } @@ -264,6 +270,12 @@ func migrateDB() error { } func migrateDBFast() error { + // 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录 + if common.UsingMySQL { + _ = DB.Exec("ALTER TABLE models DROP INDEX uk_model_name;").Error + _ = DB.Exec("ALTER TABLE vendors DROP INDEX uk_vendor_name;").Error + } + var wg sync.WaitGroup migrations := []struct { diff --git a/model/model_meta.go b/model/model_meta.go index 5ccd80c5..53b00f28 100644 --- a/model/model_meta.go +++ b/model/model_meta.go @@ -36,7 +36,7 @@ type BoundChannel struct { type Model struct { Id int `json:"id"` - ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,where:deleted_at IS NULL"` + ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,priority:1"` Description string `json:"description,omitempty" gorm:"type:text"` Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"` VendorID int `json:"vendor_id,omitempty" gorm:"index"` @@ -44,7 +44,7 @@ type Model struct { Status int `json:"status" gorm:"default:1"` CreatedTime int64 `json:"created_time" gorm:"bigint"` UpdatedTime int64 `json:"updated_time" gorm:"bigint"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index;uniqueIndex:uk_model_name,priority:2"` BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"` EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"` diff --git a/model/vendor_meta.go b/model/vendor_meta.go index fd316156..b96b1d5c 100644 --- a/model/vendor_meta.go +++ b/model/vendor_meta.go @@ -14,13 +14,13 @@ import ( type Vendor struct { Id int `json:"id"` - Name string `json:"name" gorm:"size:128;not null;uniqueIndex:uk_vendor_name,where:deleted_at IS NULL"` + Name string `json:"name" gorm:"size:128;not null;uniqueIndex:uk_vendor_name,priority:1"` Description string `json:"description,omitempty" gorm:"type:text"` Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"` Status int `json:"status" gorm:"default:1"` CreatedTime int64 `json:"created_time" gorm:"bigint"` UpdatedTime int64 `json:"updated_time" gorm:"bigint"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index;uniqueIndex:uk_vendor_name,priority:2"` } // Insert 创建新的供应商记录