2023-04-22 20:39:27 +08:00
package model
import (
2025-06-14 17:51:05 +08:00
"fmt"
2024-03-04 19:32:59 +08:00
"log"
2023-04-22 21:14:09 +08:00
"one-api/common"
2025-04-03 18:57:15 +08:00
"one-api/constant"
2023-04-22 20:39:27 +08:00
"os"
2023-08-12 19:20:12 +08:00
"strings"
2024-03-04 19:32:59 +08:00
"sync"
2023-08-12 10:05:25 +08:00
"time"
2025-03-12 21:08:47 +08:00
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
2023-04-22 20:39:27 +08:00
)
2025-06-14 17:51:05 +08:00
var commonGroupCol string
var commonKeyCol string
2025-06-14 18:15:45 +08:00
var commonTrueVal string
var commonFalseVal string
2025-06-14 17:51:05 +08:00
var logKeyCol string
var logGroupCol string
2024-12-30 17:10:48 +08:00
2024-12-31 02:06:30 +08:00
func initCol ( ) {
2025-06-14 17:51:05 +08:00
// init common column names
2024-12-30 17:10:48 +08:00
if common . UsingPostgreSQL {
2025-06-14 17:51:05 +08:00
commonGroupCol = ` "group" `
commonKeyCol = ` "key" `
2025-06-14 18:15:45 +08:00
commonTrueVal = "true"
commonFalseVal = "false"
2024-12-30 17:10:48 +08:00
} else {
2025-06-14 17:51:05 +08:00
commonGroupCol = "`group`"
commonKeyCol = "`key`"
2025-06-14 18:15:45 +08:00
commonTrueVal = "1"
commonFalseVal = "0"
2025-06-14 17:51:05 +08:00
}
2025-06-14 18:15:45 +08:00
if os . Getenv ( "LOG_SQL_DSN" ) != "" {
2025-06-14 17:51:05 +08:00
switch common . LogSqlType {
case common . DatabaseTypePostgreSQL :
logGroupCol = ` "group" `
logKeyCol = ` "key" `
default :
logGroupCol = commonGroupCol
logKeyCol = commonKeyCol
}
2025-06-19 17:17:32 +08:00
} else {
// LOG_SQL_DSN 为空时,日志数据库与主数据库相同
if common . UsingPostgreSQL {
logGroupCol = ` "group" `
logKeyCol = ` "key" `
} else {
logGroupCol = commonGroupCol
logKeyCol = commonKeyCol
}
2024-12-30 17:10:48 +08:00
}
2025-06-14 17:51:05 +08:00
// log sql type and database type
2025-06-16 00:37:22 +08:00
//common.SysLog("Using Log SQL Type: " + common.LogSqlType)
2024-12-30 17:10:48 +08:00
}
2023-04-22 20:39:27 +08:00
var DB * gorm . DB
2024-08-13 10:29:55 +08:00
var LOG_DB * gorm . DB
🐛 fix(db): allow re-adding models & vendors after soft delete; add safe index cleanup
Replace legacy single-column unique indexes with composite unique indexes on
(name, deleted_at) and introduce a safe index drop utility to eliminate
duplicate-key errors and noisy MySQL 1091 warnings.
WHAT
• model/model_meta.go
- Model.ModelName → `uniqueIndex:uk_model_name,priority:1`
- Model.DeletedAt → `index; uniqueIndex:uk_model_name,priority:2`
• model/vendor_meta.go
- Vendor.Name → `uniqueIndex:uk_vendor_name,priority:1`
- Vendor.DeletedAt → `index; uniqueIndex:uk_vendor_name,priority:2`
• model/main.go
- Add `dropIndexIfExists(table, index)`:
• Checks `information_schema.statistics`
• Drops index only when present (avoids Error 1091)
- Invoke helper in `migrateDB` & `migrateDBFast`
- Remove direct `ALTER TABLE … DROP INDEX …` calls
WHY
• Users received `Error 1062 (23000)` when re-creating a soft-deleted
model/vendor because the old unique index enforced uniqueness on name alone.
• Directly dropping nonexistent indexes caused MySQL `Error 1091` noise.
HOW
• Composite unique indexes `(model_name, deleted_at)` / `(name, deleted_at)`
respect GORM soft deletes.
• Safe helper ensures idempotent migrations across environments.
RESULT
• Users can now delete and re-add the same model or vendor without manual SQL.
• Startup migration runs quietly across MySQL, PostgreSQL, and SQLite.
• No behavior changes for existing data beyond index updates.
TEST
1. Add model “deepseek-chat” → delete (soft) → re-add → success.
2. Add vendor “DeepSeek” → delete (soft) → re-add → success.
3. Restart service twice → no duplicate key or 1091 errors.
2025-08-09 15:44:08 +08:00
// dropIndexIfExists drops a MySQL index only if it exists to avoid noisy 1091 errors
func dropIndexIfExists ( tableName string , indexName string ) {
2025-08-11 01:25:13 +08:00
if ! common . UsingMySQL {
return
}
var count int64
// Check index existence via information_schema
err := DB . Raw (
"SELECT COUNT(1) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?" ,
tableName , indexName ,
) . Scan ( & count ) . Error
if err == nil && count > 0 {
_ = DB . Exec ( "ALTER TABLE " + tableName + " DROP INDEX " + indexName + ";" ) . Error
}
🐛 fix(db): allow re-adding models & vendors after soft delete; add safe index cleanup
Replace legacy single-column unique indexes with composite unique indexes on
(name, deleted_at) and introduce a safe index drop utility to eliminate
duplicate-key errors and noisy MySQL 1091 warnings.
WHAT
• model/model_meta.go
- Model.ModelName → `uniqueIndex:uk_model_name,priority:1`
- Model.DeletedAt → `index; uniqueIndex:uk_model_name,priority:2`
• model/vendor_meta.go
- Vendor.Name → `uniqueIndex:uk_vendor_name,priority:1`
- Vendor.DeletedAt → `index; uniqueIndex:uk_vendor_name,priority:2`
• model/main.go
- Add `dropIndexIfExists(table, index)`:
• Checks `information_schema.statistics`
• Drops index only when present (avoids Error 1091)
- Invoke helper in `migrateDB` & `migrateDBFast`
- Remove direct `ALTER TABLE … DROP INDEX …` calls
WHY
• Users received `Error 1062 (23000)` when re-creating a soft-deleted
model/vendor because the old unique index enforced uniqueness on name alone.
• Directly dropping nonexistent indexes caused MySQL `Error 1091` noise.
HOW
• Composite unique indexes `(model_name, deleted_at)` / `(name, deleted_at)`
respect GORM soft deletes.
• Safe helper ensures idempotent migrations across environments.
RESULT
• Users can now delete and re-add the same model or vendor without manual SQL.
• Startup migration runs quietly across MySQL, PostgreSQL, and SQLite.
• No behavior changes for existing data beyond index updates.
TEST
1. Add model “deepseek-chat” → delete (soft) → re-add → success.
2. Add vendor “DeepSeek” → delete (soft) → re-add → success.
3. Restart service twice → no duplicate key or 1091 errors.
2025-08-09 15:44:08 +08:00
}
2023-04-22 20:39:27 +08:00
func createRootAccountIfNeed ( ) error {
var user User
//if user.Status != common.UserStatusEnabled {
if err := DB . First ( & user ) . Error ; err != nil {
common . SysLog ( "no user exists, create a root user for you: username is root, password is 123456" )
hashedPassword , err := common . Password2Hash ( "123456" )
if err != nil {
return err
}
rootUser := User {
Username : "root" ,
Password : hashedPassword ,
Role : common . RoleRootUser ,
Status : common . UserStatusEnabled ,
DisplayName : "Root User" ,
2024-09-24 19:44:30 +08:00
AccessToken : nil ,
2023-05-21 10:05:34 +08:00
Quota : 100000000 ,
2023-04-22 20:39:27 +08:00
}
DB . Create ( & rootUser )
}
return nil
}
2025-04-08 18:14:36 +08:00
func CheckSetup ( ) {
2025-04-04 21:27:24 +08:00
setup := GetSetup ( )
if setup == nil {
// No setup record exists, check if we have a root user
2025-04-03 18:57:15 +08:00
if RootUserExists ( ) {
common . SysLog ( "system is not initialized, but root user exists" )
// Create setup record
2025-04-04 21:27:24 +08:00
newSetup := Setup {
2025-04-03 18:57:15 +08:00
Version : common . Version ,
InitializedAt : time . Now ( ) . Unix ( ) ,
}
2025-04-04 21:27:24 +08:00
err := DB . Create ( & newSetup ) . Error
2025-04-03 18:57:15 +08:00
if err != nil {
common . SysLog ( "failed to create setup record: " + err . Error ( ) )
}
constant . Setup = true
} else {
2025-04-04 21:27:24 +08:00
common . SysLog ( "system is not initialized and no root user exists" )
2025-04-03 18:57:15 +08:00
constant . Setup = false
}
2025-04-04 21:27:24 +08:00
} else {
// Setup record exists, system is initialized
common . SysLog ( "system is already initialized at: " + time . Unix ( setup . InitializedAt , 0 ) . String ( ) )
constant . Setup = true
2025-04-03 18:57:15 +08:00
}
}
2025-06-14 17:51:05 +08:00
func chooseDB ( envName string , isLog bool ) ( * gorm . DB , error ) {
2024-12-31 02:06:30 +08:00
defer func ( ) {
initCol ( )
} ( )
2024-08-13 10:29:55 +08:00
dsn := os . Getenv ( envName )
if dsn != "" {
2025-03-12 21:08:47 +08:00
if strings . HasPrefix ( dsn , "postgres://" ) || strings . HasPrefix ( dsn , "postgresql://" ) {
2023-08-12 19:20:12 +08:00
// Use PostgreSQL
common . SysLog ( "using PostgreSQL as database" )
2025-06-14 17:51:05 +08:00
if ! isLog {
common . UsingPostgreSQL = true
} else {
common . LogSqlType = common . DatabaseTypePostgreSQL
}
2023-08-12 19:20:12 +08:00
return gorm . Open ( postgres . New ( postgres . Config {
DSN : dsn ,
PreferSimpleProtocol : true , // disables implicit prepared statement usage
} ) , & gorm . Config {
PrepareStmt : true , // precompile SQL
} )
}
2024-08-13 10:29:55 +08:00
if strings . HasPrefix ( dsn , "local" ) {
common . SysLog ( "SQL_DSN not set, using SQLite as database" )
2025-06-14 17:51:05 +08:00
if ! isLog {
common . UsingSQLite = true
} else {
common . LogSqlType = common . DatabaseTypeSQLite
}
2024-08-13 10:29:55 +08:00
return gorm . Open ( sqlite . Open ( common . SQLitePath ) , & gorm . Config {
PrepareStmt : true , // precompile SQL
} )
}
2023-04-22 20:39:27 +08:00
// Use MySQL
2023-06-22 01:12:28 +08:00
common . SysLog ( "using MySQL as database" )
2023-12-27 15:33:35 +08:00
// check parseTime
if ! strings . Contains ( dsn , "parseTime" ) {
2023-12-27 16:47:32 +08:00
if strings . Contains ( dsn , "?" ) {
dsn += "&parseTime=true"
} else {
dsn += "?parseTime=true"
}
2023-12-27 15:33:35 +08:00
}
2025-06-14 17:51:05 +08:00
if ! isLog {
common . UsingMySQL = true
} else {
common . LogSqlType = common . DatabaseTypeMySQL
}
2023-08-12 19:20:12 +08:00
return gorm . Open ( mysql . Open ( dsn ) , & gorm . Config {
2023-04-22 20:39:27 +08:00
PrepareStmt : true , // precompile SQL
} )
}
2023-08-12 19:20:12 +08:00
// Use SQLite
common . SysLog ( "SQL_DSN not set, using SQLite as database" )
common . UsingSQLite = true
return gorm . Open ( sqlite . Open ( common . SQLitePath ) , & gorm . Config {
PrepareStmt : true , // precompile SQL
} )
}
func InitDB ( ) ( err error ) {
2025-06-14 17:51:05 +08:00
db , err := chooseDB ( "SQL_DSN" , false )
2023-04-22 20:39:27 +08:00
if err == nil {
2023-08-12 18:10:15 +08:00
if common . DebugEnabled {
db = db . Debug ( )
}
2023-04-22 20:39:27 +08:00
DB = db
2025-08-12 14:12:11 +08:00
// MySQL charset/collation startup check: ensure Chinese-capable charset
if common . UsingMySQL {
if err := checkMySQLChineseSupport ( DB ) ; err != nil {
panic ( err )
}
}
2023-08-12 10:05:25 +08:00
sqlDB , err := DB . DB ( )
if err != nil {
return err
}
2024-06-27 19:30:17 +08:00
sqlDB . SetMaxIdleConns ( common . GetEnvOrDefault ( "SQL_MAX_IDLE_CONNS" , 100 ) )
sqlDB . SetMaxOpenConns ( common . GetEnvOrDefault ( "SQL_MAX_OPEN_CONNS" , 1000 ) )
sqlDB . SetConnMaxLifetime ( time . Second * time . Duration ( common . GetEnvOrDefault ( "SQL_MAX_LIFETIME" , 60 ) ) )
2023-08-12 10:05:25 +08:00
2023-06-22 00:52:27 +08:00
if ! common . IsMasterNode {
return nil
}
2025-02-06 14:35:14 +08:00
if common . UsingMySQL {
2025-06-14 17:51:05 +08:00
//_, _ = sqlDB.Exec("ALTER TABLE channels MODIFY model_mapping TEXT;") // TODO: delete this line when most users have upgraded
2025-02-06 14:35:14 +08:00
}
2023-10-02 12:13:30 +08:00
common . SysLog ( "database migration started" )
2024-08-13 10:29:55 +08:00
err = migrateDB ( )
return err
} else {
common . FatalLog ( err )
}
return err
}
func InitLogDB ( ) ( err error ) {
if os . Getenv ( "LOG_SQL_DSN" ) == "" {
LOG_DB = DB
return
}
2025-06-14 17:51:05 +08:00
db , err := chooseDB ( "LOG_SQL_DSN" , true )
2024-08-13 10:29:55 +08:00
if err == nil {
if common . DebugEnabled {
db = db . Debug ( )
2023-08-14 22:16:32 +08:00
}
2024-08-13 10:29:55 +08:00
LOG_DB = db
2025-08-12 14:12:11 +08:00
// If log DB is MySQL, also ensure Chinese-capable charset
if common . LogSqlType == common . DatabaseTypeMySQL {
if err := checkMySQLChineseSupport ( LOG_DB ) ; err != nil {
panic ( err )
}
}
2024-08-13 10:29:55 +08:00
sqlDB , err := LOG_DB . DB ( )
2024-01-07 18:31:14 +08:00
if err != nil {
return err
}
2024-08-13 10:29:55 +08:00
sqlDB . SetMaxIdleConns ( common . GetEnvOrDefault ( "SQL_MAX_IDLE_CONNS" , 100 ) )
sqlDB . SetMaxOpenConns ( common . GetEnvOrDefault ( "SQL_MAX_OPEN_CONNS" , 1000 ) )
sqlDB . SetConnMaxLifetime ( time . Second * time . Duration ( common . GetEnvOrDefault ( "SQL_MAX_LIFETIME" , 60 ) ) )
if ! common . IsMasterNode {
return nil
2024-06-12 20:37:42 +08:00
}
2024-08-13 10:29:55 +08:00
common . SysLog ( "database migration started" )
err = migrateLOGDB ( )
2023-04-22 20:39:27 +08:00
return err
} else {
common . FatalLog ( err )
}
return err
}
2024-08-13 10:29:55 +08:00
func migrateDB ( ) error {
🐛 fix(db): allow re-adding models/vendors after soft delete via composite unique indexes
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.
2025-08-09 13:07:57 +08:00
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
2025-08-11 01:25:13 +08:00
// 删除单列唯一索引(列级 UNIQUE) 及早期命名方式, 防止与新复合唯一索引 (model_name, deleted_at) 冲突
dropIndexIfExists ( "models" , "uk_model_name" ) // 新版复合索引名称(若已存在)
dropIndexIfExists ( "models" , "model_name" ) // 旧版列级唯一索引名称
dropIndexIfExists ( "vendors" , "uk_vendor_name" ) // 新版复合索引名称(若已存在)
dropIndexIfExists ( "vendors" , "name" ) // 旧版列级唯一索引名称
2025-08-12 14:12:11 +08:00
//if !common.UsingPostgreSQL {
// return migrateDBFast()
//}
2025-06-14 19:47:44 +08:00
err := DB . AutoMigrate (
& Channel { } ,
& Token { } ,
& User { } ,
& Option { } ,
& Redemption { } ,
& Ability { } ,
& Log { } ,
& Midjourney { } ,
& TopUp { } ,
& QuotaData { } ,
& Task { } ,
🚀 feat: Introduce full Model & Vendor Management suite (backend + frontend) and UI refinements
Backend
• Add `model/model_meta.go` and `model/vendor_meta.go` defining Model & Vendor entities with CRUD helpers, soft-delete and time stamps
• Create corresponding controllers `controller/model_meta.go`, `controller/vendor_meta.go` and register routes in `router/api-router.go`
• Auto-migrate new tables in DB startup logic
Frontend
• Build complete “Model Management” module under `/console/models`
- New pages, tables, filters, actions, hooks (`useModelsData`) and dynamic vendor tabs
- Modals `EditModelModal.jsx` & unified `EditVendorModal.jsx`; latter now uses default confirm/cancel footer and mobile-friendly modal sizing (`full-width` / `small`) via `useIsMobile`
• Update sidebar (`SiderBar.js`) and routing (`App.js`) to surface the feature
• Add helper updates (`render.js`) incl. `stringToColor`, dynamic LobeHub icon retrieval, and tag color palettes
Table UX improvements
• Replace separate status column with inline Enable / Disable buttons in operation column (matching channel table style)
• Limit visible tags to max 3; overflow represented as “+x” tag with padded `Popover` showing remaining tags
• Color all tags deterministically using `stringToColor` for consistent theming
• Change vendor column tag color to white for better contrast
Misc
• Minor layout tweaks, compact-mode toggle relocation, lint fixes and TypeScript/ESLint clean-up
These changes collectively deliver end-to-end model & vendor administration while unifying visual language across management tables.
2025-07-31 22:28:09 +08:00
& Model { } ,
& Vendor { } ,
2025-08-04 02:54:37 +08:00
& PrefillGroup { } ,
2025-06-14 19:47:44 +08:00
& Setup { } ,
2025-08-02 14:53:28 +08:00
& TwoFA { } ,
& TwoFABackupCode { } ,
2025-06-14 19:47:44 +08:00
)
if err != nil {
return err
}
return nil
}
func migrateDBFast ( ) error {
🐛 fix(db): allow re-adding models/vendors after soft delete via composite unique indexes
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.
2025-08-09 13:07:57 +08:00
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
2025-08-11 01:25:13 +08:00
// 删除单列唯一索引(列级 UNIQUE) 及早期命名方式, 防止与新复合唯一索引冲突
🐛 fix(db): allow re-adding models & vendors after soft delete; add safe index cleanup
Replace legacy single-column unique indexes with composite unique indexes on
(name, deleted_at) and introduce a safe index drop utility to eliminate
duplicate-key errors and noisy MySQL 1091 warnings.
WHAT
• model/model_meta.go
- Model.ModelName → `uniqueIndex:uk_model_name,priority:1`
- Model.DeletedAt → `index; uniqueIndex:uk_model_name,priority:2`
• model/vendor_meta.go
- Vendor.Name → `uniqueIndex:uk_vendor_name,priority:1`
- Vendor.DeletedAt → `index; uniqueIndex:uk_vendor_name,priority:2`
• model/main.go
- Add `dropIndexIfExists(table, index)`:
• Checks `information_schema.statistics`
• Drops index only when present (avoids Error 1091)
- Invoke helper in `migrateDB` & `migrateDBFast`
- Remove direct `ALTER TABLE … DROP INDEX …` calls
WHY
• Users received `Error 1062 (23000)` when re-creating a soft-deleted
model/vendor because the old unique index enforced uniqueness on name alone.
• Directly dropping nonexistent indexes caused MySQL `Error 1091` noise.
HOW
• Composite unique indexes `(model_name, deleted_at)` / `(name, deleted_at)`
respect GORM soft deletes.
• Safe helper ensures idempotent migrations across environments.
RESULT
• Users can now delete and re-add the same model or vendor without manual SQL.
• Startup migration runs quietly across MySQL, PostgreSQL, and SQLite.
• No behavior changes for existing data beyond index updates.
TEST
1. Add model “deepseek-chat” → delete (soft) → re-add → success.
2. Add vendor “DeepSeek” → delete (soft) → re-add → success.
3. Restart service twice → no duplicate key or 1091 errors.
2025-08-09 15:44:08 +08:00
dropIndexIfExists ( "models" , "uk_model_name" )
2025-08-11 01:25:13 +08:00
dropIndexIfExists ( "models" , "model_name" )
🐛 fix(db): allow re-adding models & vendors after soft delete; add safe index cleanup
Replace legacy single-column unique indexes with composite unique indexes on
(name, deleted_at) and introduce a safe index drop utility to eliminate
duplicate-key errors and noisy MySQL 1091 warnings.
WHAT
• model/model_meta.go
- Model.ModelName → `uniqueIndex:uk_model_name,priority:1`
- Model.DeletedAt → `index; uniqueIndex:uk_model_name,priority:2`
• model/vendor_meta.go
- Vendor.Name → `uniqueIndex:uk_vendor_name,priority:1`
- Vendor.DeletedAt → `index; uniqueIndex:uk_vendor_name,priority:2`
• model/main.go
- Add `dropIndexIfExists(table, index)`:
• Checks `information_schema.statistics`
• Drops index only when present (avoids Error 1091)
- Invoke helper in `migrateDB` & `migrateDBFast`
- Remove direct `ALTER TABLE … DROP INDEX …` calls
WHY
• Users received `Error 1062 (23000)` when re-creating a soft-deleted
model/vendor because the old unique index enforced uniqueness on name alone.
• Directly dropping nonexistent indexes caused MySQL `Error 1091` noise.
HOW
• Composite unique indexes `(model_name, deleted_at)` / `(name, deleted_at)`
respect GORM soft deletes.
• Safe helper ensures idempotent migrations across environments.
RESULT
• Users can now delete and re-add the same model or vendor without manual SQL.
• Startup migration runs quietly across MySQL, PostgreSQL, and SQLite.
• No behavior changes for existing data beyond index updates.
TEST
1. Add model “deepseek-chat” → delete (soft) → re-add → success.
2. Add vendor “DeepSeek” → delete (soft) → re-add → success.
3. Restart service twice → no duplicate key or 1091 errors.
2025-08-09 15:44:08 +08:00
dropIndexIfExists ( "vendors" , "uk_vendor_name" )
2025-08-11 01:25:13 +08:00
dropIndexIfExists ( "vendors" , "name" )
🐛 fix(db): allow re-adding models/vendors after soft delete via composite unique indexes
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.
2025-08-09 13:07:57 +08:00
2025-06-14 17:51:05 +08:00
var wg sync . WaitGroup
migrations := [ ] struct {
model interface { }
name string
} {
{ & Channel { } , "Channel" } ,
{ & Token { } , "Token" } ,
{ & User { } , "User" } ,
{ & Option { } , "Option" } ,
{ & Redemption { } , "Redemption" } ,
{ & Ability { } , "Ability" } ,
{ & Log { } , "Log" } ,
{ & Midjourney { } , "Midjourney" } ,
{ & TopUp { } , "TopUp" } ,
{ & QuotaData { } , "QuotaData" } ,
{ & Task { } , "Task" } ,
🚀 feat: Introduce full Model & Vendor Management suite (backend + frontend) and UI refinements
Backend
• Add `model/model_meta.go` and `model/vendor_meta.go` defining Model & Vendor entities with CRUD helpers, soft-delete and time stamps
• Create corresponding controllers `controller/model_meta.go`, `controller/vendor_meta.go` and register routes in `router/api-router.go`
• Auto-migrate new tables in DB startup logic
Frontend
• Build complete “Model Management” module under `/console/models`
- New pages, tables, filters, actions, hooks (`useModelsData`) and dynamic vendor tabs
- Modals `EditModelModal.jsx` & unified `EditVendorModal.jsx`; latter now uses default confirm/cancel footer and mobile-friendly modal sizing (`full-width` / `small`) via `useIsMobile`
• Update sidebar (`SiderBar.js`) and routing (`App.js`) to surface the feature
• Add helper updates (`render.js`) incl. `stringToColor`, dynamic LobeHub icon retrieval, and tag color palettes
Table UX improvements
• Replace separate status column with inline Enable / Disable buttons in operation column (matching channel table style)
• Limit visible tags to max 3; overflow represented as “+x” tag with padded `Popover` showing remaining tags
• Color all tags deterministically using `stringToColor` for consistent theming
• Change vendor column tag color to white for better contrast
Misc
• Minor layout tweaks, compact-mode toggle relocation, lint fixes and TypeScript/ESLint clean-up
These changes collectively deliver end-to-end model & vendor administration while unifying visual language across management tables.
2025-07-31 22:28:09 +08:00
{ & Model { } , "Model" } ,
2025-08-11 01:25:13 +08:00
{ & Vendor { } , "Vendor" } ,
2025-08-04 02:54:37 +08:00
{ & PrefillGroup { } , "PrefillGroup" } ,
2025-06-14 17:51:05 +08:00
{ & Setup { } , "Setup" } ,
2025-08-02 14:53:28 +08:00
{ & TwoFA { } , "TwoFA" } ,
{ & TwoFABackupCode { } , "TwoFABackupCode" } ,
2024-08-13 10:29:55 +08:00
}
2025-07-20 10:11:35 +08:00
// 动态计算migration数量, 确保errChan缓冲区足够大
errChan := make ( chan error , len ( migrations ) )
2025-06-14 17:51:05 +08:00
for _ , m := range migrations {
wg . Add ( 1 )
go func ( model interface { } , name string ) {
defer wg . Done ( )
if err := DB . AutoMigrate ( model ) ; err != nil {
errChan <- fmt . Errorf ( "failed to migrate %s: %v" , name , err )
}
} ( m . model , m . name )
2024-08-13 10:29:55 +08:00
}
2025-06-14 17:51:05 +08:00
// Wait for all migrations to complete
wg . Wait ( )
close ( errChan )
// Check for any errors
for err := range errChan {
if err != nil {
return err
}
2024-08-13 10:29:55 +08:00
}
common . SysLog ( "database migrated" )
2025-06-14 17:51:05 +08:00
return nil
2024-08-13 10:29:55 +08:00
}
func migrateLOGDB ( ) error {
var err error
if err = LOG_DB . AutoMigrate ( & Log { } ) ; err != nil {
return err
}
return nil
}
func closeDB ( db * gorm . DB ) error {
sqlDB , err := db . DB ( )
2023-04-22 20:39:27 +08:00
if err != nil {
return err
}
err = sqlDB . Close ( )
return err
}
2024-03-04 19:32:59 +08:00
2024-08-13 10:29:55 +08:00
func CloseDB ( ) error {
if LOG_DB != DB {
err := closeDB ( LOG_DB )
if err != nil {
return err
}
}
return closeDB ( DB )
}
2025-08-12 14:12:11 +08:00
// checkMySQLChineseSupport ensures the MySQL connection and current schema
// default charset/collation can store Chinese characters. It allows common
// Chinese-capable charsets (utf8mb4, utf8, gbk, big5, gb18030) and panics otherwise.
func checkMySQLChineseSupport ( db * gorm . DB ) error {
2025-08-12 16:31:00 +08:00
// 仅检测:当前库默认字符集/排序规则 + 各表的排序规则(隐含字符集)
2025-08-12 14:12:11 +08:00
// Read current schema defaults
var schemaCharset , schemaCollation string
err := db . Raw ( "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = DATABASE()" ) . Row ( ) . Scan ( & schemaCharset , & schemaCollation )
if err != nil {
return fmt . Errorf ( "读取当前库默认字符集/排序规则失败 / Failed to read schema default charset/collation: %v" , err )
}
toLower := func ( s string ) string { return strings . ToLower ( s ) }
// Allowed charsets that can store Chinese text
allowedCharsets := map [ string ] string {
"utf8mb4" : "utf8mb4_" ,
"utf8" : "utf8_" ,
"gbk" : "gbk_" ,
"big5" : "big5_" ,
"gb18030" : "gb18030_" ,
}
isChineseCapable := func ( cs , cl string ) bool {
csLower := toLower ( cs )
clLower := toLower ( cl )
if prefix , ok := allowedCharsets [ csLower ] ; ok {
if clLower == "" {
return true
}
return strings . HasPrefix ( clLower , prefix )
}
2025-08-12 16:31:00 +08:00
// 如果仅提供了排序规则,尝试按排序规则前缀判断
for _ , prefix := range allowedCharsets {
if strings . HasPrefix ( clLower , prefix ) {
return true
}
}
2025-08-12 14:12:11 +08:00
return false
}
2025-08-12 16:31:00 +08:00
// 1) 当前库默认值必须支持中文
if ! isChineseCapable ( schemaCharset , schemaCollation ) {
return fmt . Errorf ( "当前库默认字符集/排序规则不支持中文: schema(%s/%s)。请将库设置为 utf8mb4/utf8/gbk/big5/gb18030 / Schema default charset/collation is not Chinese-capable: schema(%s/%s). Please set to utf8mb4/utf8/gbk/big5/gb18030" ,
schemaCharset , schemaCollation , schemaCharset , schemaCollation )
}
// 2) 所有物理表的排序规则(隐含字符集)必须支持中文
type tableInfo struct {
Name string
Collation * string
}
var tables [ ] tableInfo
if err := db . Raw ( "SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'" ) . Scan ( & tables ) . Error ; err != nil {
return fmt . Errorf ( "读取表排序规则失败 / Failed to read table collations: %v" , err )
}
var badTables [ ] string
for _ , t := range tables {
// NULL 或空表示继承库默认设置,已在上面校验库默认,视为通过
if t . Collation == nil || * t . Collation == "" {
continue
}
cl := * t . Collation
// 仅凭排序规则判断是否中文可用
ok := false
lower := strings . ToLower ( cl )
for _ , prefix := range allowedCharsets {
if strings . HasPrefix ( lower , prefix ) {
ok = true
break
}
}
if ! ok {
badTables = append ( badTables , fmt . Sprintf ( "%s(%s)" , t . Name , cl ) )
}
}
if len ( badTables ) > 0 {
// 限制输出数量以避免日志过长
maxShow := 20
shown := badTables
if len ( shown ) > maxShow {
shown = shown [ : maxShow ]
}
return fmt . Errorf (
"存在不支持中文的表,请修复其排序规则/字符集。示例(最多展示 %d 项):%v / Found tables not Chinese-capable. Please fix their collation/charset. Examples (showing up to %d): %v" ,
maxShow , shown , maxShow , shown ,
)
2025-08-12 14:12:11 +08:00
}
return nil
}
2024-03-04 19:32:59 +08:00
var (
lastPingTime time . Time
pingMutex sync . Mutex
)
func PingDB ( ) error {
pingMutex . Lock ( )
defer pingMutex . Unlock ( )
if time . Since ( lastPingTime ) < time . Second * 10 {
return nil
}
sqlDB , err := DB . DB ( )
if err != nil {
log . Printf ( "Error getting sql.DB from GORM: %v" , err )
return err
}
err = sqlDB . Ping ( )
if err != nil {
log . Printf ( "Error pinging DB: %v" , err )
return err
}
lastPingTime = time . Now ( )
common . SysLog ( "Database pinged successfully" )
return nil
}