mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-27 20:53:01 +00:00
feat(db): split inbound tables to optional mysql with sqlite fallback
This commit is contained in:
parent
5b796672e9
commit
f57364e78b
6 changed files with 306 additions and 100 deletions
13
.env.example
13
.env.example
|
|
@ -1,4 +1,15 @@
|
|||
XUI_DEBUG=true
|
||||
XUI_DB_FOLDER=x-ui
|
||||
XUI_LOG_FOLDER=x-ui
|
||||
XUI_BIN_FOLDER=x-ui
|
||||
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
|
||||
|
|
|
|||
143
database/db.go
143
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
|
||||
|
|
|
|||
1
go.mod
1
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue