Files
new-api/model/main.go

509 lines
13 KiB
Go
Raw Permalink Normal View History

2023-04-22 20:39:27 +08:00
package model
import (
"fmt"
2024-03-04 19:32:59 +08:00
"log"
2023-04-22 21:14:09 +08:00
"one-api/common"
"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"
"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
)
var commonGroupCol string
var commonKeyCol string
var commonTrueVal string
var commonFalseVal string
var logKeyCol string
var logGroupCol string
2024-12-30 17:10:48 +08:00
2024-12-31 02:06:30 +08:00
func initCol() {
// init common column names
2024-12-30 17:10:48 +08:00
if common.UsingPostgreSQL {
commonGroupCol = `"group"`
commonKeyCol = `"key"`
commonTrueVal = "true"
commonFalseVal = "false"
2024-12-30 17:10:48 +08:00
} else {
commonGroupCol = "`group`"
commonKeyCol = "`key`"
commonTrueVal = "1"
commonFalseVal = "0"
}
if os.Getenv("LOG_SQL_DSN") != "" {
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
}
// log sql type and database type
//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) {
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",
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
}
func CheckSetup() {
setup := GetSetup()
if setup == nil {
// No setup record exists, check if we have a root user
if RootUserExists() {
common.SysLog("system is not initialized, but root user exists")
// Create setup record
newSetup := Setup{
Version: common.Version,
InitializedAt: time.Now().Unix(),
}
err := DB.Create(&newSetup).Error
if err != nil {
common.SysLog("failed to create setup record: " + err.Error())
}
constant.Setup = true
} else {
common.SysLog("system is not initialized and no root user exists")
constant.Setup = false
}
} else {
// Setup record exists, system is initialized
common.SysLog("system is already initialized at: " + time.Unix(setup.InitializedAt, 0).String())
constant.Setup = true
}
}
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")
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")
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
}
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) {
db, err := chooseDB("SQL_DSN", false)
2023-04-22 20:39:27 +08:00
if err == nil {
if common.DebugEnabled {
db = db.Debug()
}
2023-04-22 20:39:27 +08:00
DB = db
// MySQL charset/collation startup check: ensure Chinese-capable charset
if common.UsingMySQL {
if err := checkMySQLChineseSupport(DB); err != nil {
panic(err)
}
}
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)))
if !common.IsMasterNode {
return nil
}
if common.UsingMySQL {
//_, _ = sqlDB.Exec("ALTER TABLE channels MODIFY model_mapping TEXT;") // TODO: delete this line when most users have upgraded
}
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
}
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
// 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-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
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
// 删除单列唯一索引(列级 UNIQUE及早期命名方式防止与新复合唯一索引 (model_name, deleted_at) 冲突
dropIndexIfExists("models", "uk_model_name") // 新版复合索引名称(若已存在)
dropIndexIfExists("models", "model_name") // 旧版列级唯一索引名称
dropIndexIfExists("vendors", "uk_vendor_name") // 新版复合索引名称(若已存在)
dropIndexIfExists("vendors", "name") // 旧版列级唯一索引名称
//if !common.UsingPostgreSQL {
// return migrateDBFast()
//}
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{},
&PrefillGroup{},
&Setup{},
&TwoFA{},
&TwoFABackupCode{},
)
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
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
// 删除单列唯一索引(列级 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")
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")
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
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"},
{&Vendor{}, "Vendor"},
{&PrefillGroup{}, "PrefillGroup"},
{&Setup{}, "Setup"},
{&TwoFA{}, "TwoFA"},
{&TwoFABackupCode{}, "TwoFABackupCode"},
2024-08-13 10:29:55 +08:00
}
// 动态计算migration数量确保errChan缓冲区足够大
errChan := make(chan error, len(migrations))
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
}
// 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")
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)
}
// 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 {
// 仅检测:当前库默认字符集/排序规则 + 各表的排序规则(隐含字符集)
// 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)
}
// 如果仅提供了排序规则,尝试按排序规则前缀判断
for _, prefix := range allowedCharsets {
if strings.HasPrefix(clLower, prefix) {
return true
}
}
return false
}
// 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,
)
}
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
}