From f57364e78b8d1e2c1259d11896206f5923eaf039 Mon Sep 17 00:00:00 2001 From: Yasin Abbasi Date: Mon, 16 Feb 2026 01:26:54 +0330 Subject: [PATCH] feat(db): split inbound tables to optional mysql with sqlite fallback --- .env.example | 13 ++- database/db.go | 143 +++++++++++++++++++++++++-- go.mod | 1 + sub/subService.go | 70 +++++++++---- web/job/check_client_ip_job.go | 4 +- web/service/inbound.go | 175 +++++++++++++++++++-------------- 6 files changed, 306 insertions(+), 100 deletions(-) diff --git a/.env.example b/.env.example index 62aa8c0b..7e1fcbd3 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,15 @@ XUI_DEBUG=true XUI_DB_FOLDER=x-ui XUI_LOG_FOLDER=x-ui -XUI_BIN_FOLDER=x-ui \ No newline at end of file +XUI_BIN_FOLDER=x-ui + +# Optional: use MySQL only for inbounds and client_traffics tables. +# If XUI_MYSQL_DSN is set, it has priority over host/user/password fields. +# Example DSN: user:pass@tcp(127.0.0.1:3306)/xui_shared?charset=utf8mb4&parseTime=True&loc=Local +XUI_MYSQL_DSN= +XUI_MYSQL_HOST= +XUI_MYSQL_PORT=3306 +XUI_MYSQL_USER= +XUI_MYSQL_PASSWORD= +XUI_MYSQL_DB= +XUI_MYSQL_PARAMS=charset=utf8mb4&parseTime=True&loc=Local diff --git a/database/db.go b/database/db.go index 6b579dd9..dc049223 100644 --- a/database/db.go +++ b/database/db.go @@ -1,5 +1,5 @@ // Package database provides database initialization, migration, and management utilities -// for the 3x-ui panel using GORM with SQLite. +// for the 3x-ui panel using GORM with SQLite and optional MySQL split storage. package database import ( @@ -11,34 +11,38 @@ import ( "os" "path" "slices" + "strings" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/util/crypto" "github.com/mhsanaei/3x-ui/v2/xray" + "gorm.io/driver/mysql" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) var db *gorm.DB +var inboundDB *gorm.DB const ( defaultUsername = "admin" defaultPassword = "admin" ) -func initModels() error { +func initSQLiteModels(includeInboundModels bool) error { models := []any{ &model.User{}, - &model.Inbound{}, &model.OutboundTraffics{}, &model.Setting{}, &model.InboundClientIps{}, - &xray.ClientTraffic{}, &model.HistoryOfSeeders{}, } + if includeInboundModels { + models = append(models, &model.Inbound{}, &xray.ClientTraffic{}) + } for _, model := range models { if err := db.AutoMigrate(model); err != nil { log.Printf("Error auto migrating model: %v", err) @@ -48,6 +52,87 @@ func initModels() error { return nil } +func initInboundModels() error { + if inboundDB == nil { + return errors.New("inbound database is nil") + } + models := []any{ + &model.Inbound{}, + &xray.ClientTraffic{}, + } + for _, model := range models { + if err := inboundDB.AutoMigrate(model); err != nil { + log.Printf("Error auto migrating inbound model: %v", err) + return err + } + } + return nil +} + +func migrateInboundDataIfNeeded() error { + if inboundDB == nil || db == nil || inboundDB == db { + return nil + } + + var mysqlInboundCount int64 + if err := inboundDB.Model(&model.Inbound{}).Count(&mysqlInboundCount).Error; err != nil { + return err + } + if mysqlInboundCount == 0 { + var sqliteInbounds []model.Inbound + if err := db.Model(&model.Inbound{}).Find(&sqliteInbounds).Error; err != nil { + return err + } + if len(sqliteInbounds) > 0 { + if err := inboundDB.CreateInBatches(&sqliteInbounds, 200).Error; err != nil { + return err + } + } + } + + var mysqlClientTrafficCount int64 + if err := inboundDB.Model(&xray.ClientTraffic{}).Count(&mysqlClientTrafficCount).Error; err != nil { + return err + } + if mysqlClientTrafficCount == 0 { + var sqliteClientTraffics []xray.ClientTraffic + if err := db.Model(&xray.ClientTraffic{}).Find(&sqliteClientTraffics).Error; err != nil { + return err + } + if len(sqliteClientTraffics) > 0 { + if err := inboundDB.CreateInBatches(&sqliteClientTraffics, 500).Error; err != nil { + return err + } + } + } + + return nil +} + +func getMySQLDSN() string { + if dsn := strings.TrimSpace(os.Getenv("XUI_MYSQL_DSN")); dsn != "" { + return dsn + } + + host := strings.TrimSpace(os.Getenv("XUI_MYSQL_HOST")) + port := strings.TrimSpace(os.Getenv("XUI_MYSQL_PORT")) + user := strings.TrimSpace(os.Getenv("XUI_MYSQL_USER")) + pass := os.Getenv("XUI_MYSQL_PASSWORD") + dbName := strings.TrimSpace(os.Getenv("XUI_MYSQL_DB")) + params := strings.TrimSpace(os.Getenv("XUI_MYSQL_PARAMS")) + + if host == "" || user == "" || dbName == "" { + return "" + } + if port == "" { + port = "3306" + } + if params == "" { + params = "charset=utf8mb4&parseTime=True&loc=Local" + } + return user + ":" + pass + "@tcp(" + host + ":" + port + ")/" + dbName + "?" + params +} + // initUser creates a default admin user if the users table is empty. func initUser() error { empty, err := isTableEmpty("users") @@ -142,10 +227,29 @@ func InitDB(dbPath string) error { if err != nil { return err } + inboundDB = db - if err := initModels(); err != nil { + mysqlDSN := getMySQLDSN() + useDedicatedInboundDB := mysqlDSN != "" + if useDedicatedInboundDB { + mysqlInboundDB, mysqlErr := gorm.Open(mysql.Open(mysqlDSN), c) + if mysqlErr != nil { + return mysqlErr + } + inboundDB = mysqlInboundDB + } + + if err := initSQLiteModels(!useDedicatedInboundDB); err != nil { return err } + if useDedicatedInboundDB { + if err := initInboundModels(); err != nil { + return err + } + if err := migrateInboundDataIfNeeded(); err != nil { + return err + } + } isUsersEmpty, err := isTableEmpty("users") if err != nil { @@ -160,14 +264,31 @@ func InitDB(dbPath string) error { // CloseDB closes the database connection if it exists. func CloseDB() error { + var closeErr error + if inboundDB != nil && db != nil && inboundDB != db { + sqlInboundDB, err := inboundDB.DB() + if err != nil { + closeErr = err + } else { + closeErr = sqlInboundDB.Close() + } + } if db != nil { sqlDB, err := db.DB() if err != nil { + if closeErr != nil { + return closeErr + } + return err + } + if err = sqlDB.Close(); err != nil { + if closeErr != nil { + return closeErr + } return err } - return sqlDB.Close() } - return nil + return closeErr } // GetDB returns the global GORM database instance. @@ -175,6 +296,14 @@ func GetDB() *gorm.DB { return db } +// GetInboundDB returns the DB used for inbounds and client traffics. +func GetInboundDB() *gorm.DB { + if inboundDB != nil { + return inboundDB + } + return db +} + // IsNotFound checks if the given error is a GORM record not found error. func IsNotFound(err error) bool { return err == gorm.ErrRecordNotFound diff --git a/go.mod b/go.mod index 497c32c0..f2400f43 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( golang.org/x/sys v0.41.0 golang.org/x/text v0.34.0 google.golang.org/grpc v1.78.0 + gorm.io/driver/mysql v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) diff --git a/sub/subService.go b/sub/subService.go index 818f193b..d95fe0bd 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -113,20 +113,30 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C } func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { - db := database.GetDB() + db := database.GetInboundDB() var inbounds []*model.Inbound - err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in ( - SELECT DISTINCT inbounds.id - FROM inbounds, - JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client - WHERE - protocol in ('vmess','vless','trojan','shadowsocks') - AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ? - )`, subId, true).Find(&inbounds).Error + err := db.Model(model.Inbound{}). + Preload("ClientStats"). + Where("enable = ?", true). + Where("protocol IN ?", []model.Protocol{model.VMESS, model.VLESS, model.Trojan, model.Shadowsocks}). + Find(&inbounds).Error if err != nil { return nil, err } - return inbounds, nil + filtered := make([]*model.Inbound, 0, len(inbounds)) + for _, inbound := range inbounds { + clients, cErr := s.inboundService.GetClients(inbound) + if cErr != nil { + continue + } + for _, client := range clients { + if client.Enable && client.SubID == subId { + filtered = append(filtered, inbound) + break + } + } + } + return filtered, nil } func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic { @@ -139,16 +149,42 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri } func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) { - db := database.GetDB() - var inbound *model.Inbound - err := db.Model(model.Inbound{}). - Where("JSON_TYPE(settings, '$.fallbacks') = 'array'"). - Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest). - Find(&inbound).Error - if err != nil { + db := database.GetInboundDB() + var inbounds []*model.Inbound + if err := db.Model(model.Inbound{}).Find(&inbounds).Error; err != nil { return "", 0, "", err } + var inbound *model.Inbound + for _, candidate := range inbounds { + var settings map[string]any + if err := json.Unmarshal([]byte(candidate.Settings), &settings); err != nil { + continue + } + fallbacks, ok := settings["fallbacks"].([]any) + if !ok { + continue + } + match := false + for _, item := range fallbacks { + fallback, ok := item.(map[string]any) + if !ok { + continue + } + if fallbackDest, ok := fallback["dest"].(string); ok && fallbackDest == dest { + match = true + break + } + } + if match { + inbound = candidate + break + } + } + if inbound == nil { + return "", 0, "", fmt.Errorf("fallback master not found for dest %s", dest) + } + var stream map[string]any json.Unmarshal([]byte(streamSettings), &stream) var masterStream map[string]any diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go index d3c1a1d1..a4f6d997 100644 --- a/web/job/check_client_ip_job.go +++ b/web/job/check_client_ip_job.go @@ -94,7 +94,7 @@ func (j *CheckClientIpJob) clearAccessLog() { } func (j *CheckClientIpJob) hasLimitIp() bool { - db := database.GetDB() + db := database.GetInboundDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Find(&inbounds).Error @@ -440,7 +440,7 @@ func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, c } func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) { - db := database.GetDB() + db := database.GetInboundDB() inbound := &model.Inbound{} err := db.Model(&model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").First(inbound).Error diff --git a/web/service/inbound.go b/web/service/inbound.go index 101c79d9..9469969a 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -29,7 +29,7 @@ type InboundService struct { // GetInbounds retrieves all inbounds for a specific user. // Returns a slice of inbound models with their associated client statistics. func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { - db := database.GetDB() + db := database.GetInboundDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { @@ -60,7 +60,7 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { // GetAllInbounds retrieves all inbounds from the database. // Returns a slice of all inbound models with their associated client statistics. func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { - db := database.GetDB() + db := database.GetInboundDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { @@ -88,7 +88,7 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { } func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbound, error) { - db := database.GetDB() + db := database.GetInboundDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Where("traffic_reset = ?", period).Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { @@ -98,7 +98,7 @@ func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbo } func (s *InboundService) checkPortExist(listen string, port int, ignoreId int) (bool, error) { - db := database.GetDB() + db := database.GetInboundDB() if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" { db = db.Model(model.Inbound{}).Where("port = ?", port) } else { @@ -142,16 +142,29 @@ func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, err } func (s *InboundService) getAllEmails() ([]string, error) { - db := database.GetDB() - var emails []string - err := db.Raw(` - SELECT JSON_EXTRACT(client.value, '$.email') - FROM inbounds, - JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client - `).Scan(&emails).Error + db := database.GetInboundDB() + var inbounds []*model.Inbound + err := db.Model(model.Inbound{}).Select("settings").Find(&inbounds).Error if err != nil { return nil, err } + emailSet := make(map[string]struct{}) + for _, inbound := range inbounds { + clients, cErr := s.GetClients(inbound) + if cErr != nil { + continue + } + for _, client := range clients { + if client.Email == "" { + continue + } + emailSet[client.Email] = struct{}{} + } + } + emails := make([]string, 0, len(emailSet)) + for email := range emailSet { + emails = append(emails, email) + } return emails, nil } @@ -277,7 +290,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo } } - db := database.GetDB() + db := database.GetInboundDB() tx := db.Begin() defer func() { if err == nil { @@ -323,7 +336,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo // It removes the inbound from the database and the running Xray instance if active. // Returns whether Xray needs restart and any error. func (s *InboundService) DelInbound(id int) (bool, error) { - db := database.GetDB() + db := database.GetInboundDB() var tag string needRestart := false @@ -366,7 +379,7 @@ func (s *InboundService) DelInbound(id int) (bool, error) { } func (s *InboundService) GetInbound(id int) (*model.Inbound, error) { - db := database.GetDB() + db := database.GetInboundDB() inbound := &model.Inbound{} err := db.Model(model.Inbound{}).First(inbound, id).Error if err != nil { @@ -394,7 +407,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, tag := oldInbound.Tag - db := database.GetDB() + db := database.GetInboundDB() tx := db.Begin() defer func() { @@ -631,7 +644,7 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) { oldInbound.Settings = string(newSettings) - db := database.GetDB() + db := database.GetInboundDB() tx := db.Begin() defer func() { @@ -723,7 +736,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, oldInbound.Settings = string(newSettings) - db := database.GetDB() + db := database.GetInboundDB() err = s.DelClientIPs(db, email) if err != nil { @@ -861,7 +874,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin } oldInbound.Settings = string(newSettings) - db := database.GetDB() + db := database.GetInboundDB() tx := db.Begin() defer func() { @@ -941,7 +954,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { var err error - db := database.GetDB() + db := database.GetInboundDB() tx := db.Begin() defer func() { @@ -1300,7 +1313,7 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) } func (s *InboundService) GetInboundTags() (string, error) { - db := database.GetDB() + db := database.GetInboundDB() var inboundTags []string err := db.Model(model.Inbound{}).Select("tag").Find(&inboundTags).Error if err != nil && err != gorm.ErrRecordNotFound { @@ -1311,15 +1324,21 @@ func (s *InboundService) GetInboundTags() (string, error) { } func (s *InboundService) MigrationRemoveOrphanedTraffics() { - db := database.GetDB() - db.Exec(` - DELETE FROM client_traffics - WHERE email NOT IN ( - SELECT JSON_EXTRACT(client.value, '$.email') - FROM inbounds, - JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client - ) - `) + db := database.GetInboundDB() + emails, err := s.getAllEmails() + if err != nil { + logger.Warningf("MigrationRemoveOrphanedTraffics failed to load emails: %v", err) + return + } + if len(emails) == 0 { + if err := db.Where("1 = 1").Delete(xray.ClientTraffic{}).Error; err != nil { + logger.Warningf("MigrationRemoveOrphanedTraffics delete-all failed: %v", err) + } + return + } + if err := db.Where("email NOT IN ?", emails).Delete(xray.ClientTraffic{}).Error; err != nil { + logger.Warningf("MigrationRemoveOrphanedTraffics delete-orphans failed: %v", err) + } } func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error { @@ -1352,7 +1371,7 @@ func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *mod } func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error { - return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error + return database.GetDB().Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error } func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error { @@ -1360,11 +1379,11 @@ func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error { } func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error { - return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error + return database.GetDB().Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error } func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) { - db := database.GetDB() + db := database.GetInboundDB() var traffics []*xray.ClientTraffic err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error if err != nil { @@ -1379,7 +1398,7 @@ func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xr } func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) { - db := database.GetDB() + db := database.GetInboundDB() var traffics []*xray.ClientTraffic err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error if err != nil { @@ -1766,7 +1785,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota } func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error { - db := database.GetDB() + db := database.GetInboundDB() // Reset traffic stats in ClientTraffic table result := db.Model(xray.ClientTraffic{}). @@ -1834,7 +1853,7 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e traffic.Down = 0 traffic.Enable = true - db := database.GetDB() + db := database.GetInboundDB() err = db.Save(traffic).Error if err != nil { return false, err @@ -1844,7 +1863,7 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e } func (s *InboundService) ResetAllClientTraffics(id int) error { - db := database.GetDB() + db := database.GetInboundDB() now := time.Now().Unix() * 1000 return db.Transaction(func(tx *gorm.DB) error { @@ -1881,7 +1900,7 @@ func (s *InboundService) ResetAllClientTraffics(id int) error { } func (s *InboundService) ResetAllTraffics() error { - db := database.GetDB() + db := database.GetInboundDB() result := db.Model(model.Inbound{}). Where("user_id > ?", 0). @@ -1892,7 +1911,7 @@ func (s *InboundService) ResetAllTraffics() error { } func (s *InboundService) DelDepletedClients(id int) (err error) { - db := database.GetDB() + db := database.GetInboundDB() tx := db.Begin() defer func() { if err == nil { @@ -1977,7 +1996,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) { } func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) { - db := database.GetDB() + db := database.GetInboundDB() var inbounds []*model.Inbound // Retrieve inbounds where settings contain the given tgId @@ -2041,7 +2060,7 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl } func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64, download int64) error { - db := database.GetDB() + db := database.GetInboundDB() result := db.Model(xray.ClientTraffic{}). Where("email = ?", email). @@ -2056,16 +2075,30 @@ func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64, } func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, error) { - db := database.GetDB() + db := database.GetInboundDB() var traffics []xray.ClientTraffic - - err := db.Model(xray.ClientTraffic{}).Where(`email IN( - SELECT JSON_EXTRACT(client.value, '$.email') as email - FROM inbounds, - JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client - WHERE - JSON_EXTRACT(client.value, '$.id') in (?) - )`, id).Find(&traffics).Error + var inbounds []*model.Inbound + err := db.Model(model.Inbound{}).Find(&inbounds).Error + if err != nil { + logger.Debug(err) + return nil, err + } + emails := make([]string, 0) + for _, inbound := range inbounds { + clients, cErr := s.GetClients(inbound) + if cErr != nil { + continue + } + for _, client := range clients { + if client.ID == id && client.Email != "" { + emails = append(emails, client.Email) + } + } + } + if len(emails) == 0 { + return traffics, nil + } + err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error if err != nil { logger.Debug(err) @@ -2083,7 +2116,7 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, } func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) { - db := database.GetDB() + db := database.GetInboundDB() inbound := &model.Inbound{} traffic = &xray.ClientTraffic{} @@ -2195,7 +2228,7 @@ func (s *InboundService) ClearClientIps(clientEmail string) error { } func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) { - db := database.GetDB() + db := database.GetInboundDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { @@ -2205,13 +2238,14 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) } func (s *InboundService) MigrationRequirements() { - db := database.GetDB() + db := database.GetInboundDB() + sqliteDB := database.GetDB() tx := db.Begin() var err error defer func() { if err == nil { tx.Commit() - if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil { + if dbErr := sqliteDB.Exec(`VACUUM "main"`).Error; dbErr != nil { logger.Warningf("VACUUM failed: %v", dbErr) } } else { @@ -2313,24 +2347,17 @@ func (s *InboundService) MigrationRequirements() { tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{}) // Migrate old MultiDomain to External Proxy - var externalProxy []struct { - Id int - Port int - StreamSettings []byte - } - err = tx.Raw(`select id, port, stream_settings - from inbounds - WHERE protocol in ('vmess','vless','trojan') - AND json_extract(stream_settings, '$.security') = 'tls' - AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error - if err != nil || len(externalProxy) == 0 { - return - } - - for _, ep := range externalProxy { + for _, ep := range inbounds { var reverses any var stream map[string]any - json.Unmarshal(ep.StreamSettings, &stream) + if err := json.Unmarshal([]byte(ep.StreamSettings), &stream); err != nil { + continue + } + security, _ := stream["security"].(string) + if security != "tls" { + continue + } + if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok { if settings, ok := tlsSettings["settings"].(map[string]any); ok { if domains, ok := settings["domains"].([]any); ok { @@ -2338,7 +2365,9 @@ func (s *InboundService) MigrationRequirements() { if domainMap, ok := domain.(map[string]any); ok { domainMap["forceTls"] = "same" domainMap["port"] = ep.Port - domainMap["dest"] = domainMap["domain"].(string) + if domainVal, ok2 := domainMap["domain"].(string); ok2 { + domainMap["dest"] = domainVal + } delete(domainMap, "domain") } } @@ -2370,7 +2399,7 @@ func (s *InboundService) GetOnlineClients() []string { } func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) { - db := database.GetDB() + db := database.GetInboundDB() var rows []xray.ClientTraffic err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error if err != nil && err != gorm.ErrRecordNotFound { @@ -2384,7 +2413,7 @@ func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) { } func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) { - db := database.GetDB() + db := database.GetInboundDB() // Step 1: Get ClientTraffic records for emails in the input list var clients []xray.ClientTraffic @@ -2466,7 +2495,7 @@ func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (b oldInbound.Settings = string(newSettings) - db := database.GetDB() + db := database.GetInboundDB() // remove IP bindings if err := s.DelClientIPs(db, email); err != nil {