mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-09-12 21:20:07 +00:00
feat: Add MySQL database support
- Add MySQL database support with environment-based configuration - Fix MySQL compatibility issue with 'key' column name - Maintain SQLite as default database - Add proper validation for MySQL configuration - Test and verify compatibility with existing database - Replaced raw SQL queries using JSON_EACH functions with standard GORM queries - Modified functions to handle JSON parsing in Go code instead of database since JSON_EACH is not available on MySQL or MariaDB: - getAllEmails() - GetClientTrafficByID() - getFallbackMaster() - MigrationRemoveOrphanedTraffics() The system now supports both MySQL and SQLite databases, with SQLite remaining as the default option. MySQL connection is only used when explicitly configured through environment variables.
This commit is contained in:
parent
1b1cbfff42
commit
4d50320bc1
10 changed files with 239 additions and 52 deletions
8
.env.example
Normal file
8
.env.example
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
|
||||||
|
# If DB connection is "mysql"
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=xui
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
|
@ -3,6 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -62,7 +63,55 @@ func GetDBFolderPath() string {
|
||||||
return dbFolderPath
|
return dbFolderPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DatabaseConfig holds the database configuration
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Connection string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Database string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatabaseConfig returns the database configuration from environment variables
|
||||||
|
func GetDatabaseConfig() (*DatabaseConfig, error) {
|
||||||
|
config := &DatabaseConfig{
|
||||||
|
Connection: strings.ToLower(os.Getenv("DB_CONNECTION")),
|
||||||
|
Host: os.Getenv("DB_HOST"),
|
||||||
|
Port: os.Getenv("DB_PORT"),
|
||||||
|
Database: os.Getenv("DB_DATABASE"),
|
||||||
|
Username: os.Getenv("DB_USERNAME"),
|
||||||
|
Password: os.Getenv("DB_PASSWORD"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Connection == "mysql" {
|
||||||
|
if config.Host == "" || config.Database == "" || config.Username == "" {
|
||||||
|
return nil, fmt.Errorf("missing required MySQL configuration: host, database, and username are required")
|
||||||
|
}
|
||||||
|
if config.Port == "" {
|
||||||
|
config.Port = "3306"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetDBPath() string {
|
func GetDBPath() string {
|
||||||
|
config, err := GetDatabaseConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting database config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Connection == "mysql" {
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local",
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
config.Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection is sqlite
|
||||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,15 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
|
||||||
"x-ui/config"
|
"x-ui/config"
|
||||||
"x-ui/database/model"
|
"x-ui/database/model"
|
||||||
"x-ui/util/crypto"
|
"x-ui/util/crypto"
|
||||||
"x-ui/xray"
|
"x-ui/xray"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
|
@ -114,12 +115,22 @@ func isTableEmpty(tableName string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitDB(dbPath string) error {
|
func InitDB(dbPath string) error {
|
||||||
dir := path.Dir(dbPath)
|
dbConfig, err := config.GetDatabaseConfig()
|
||||||
err := os.MkdirAll(dir, fs.ModePerm)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dbConfig.Connection != "mysql" {
|
||||||
|
// Connection is sqlite
|
||||||
|
// Need to create the directory if it doesn't exist
|
||||||
|
|
||||||
|
dir := path.Dir(dbPath)
|
||||||
|
err = os.MkdirAll(dir, fs.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var gormLogger logger.Interface
|
var gormLogger logger.Interface
|
||||||
|
|
||||||
if config.IsDebug() {
|
if config.IsDebug() {
|
||||||
|
@ -131,10 +142,19 @@ func InitDB(dbPath string) error {
|
||||||
c := &gorm.Config{
|
c := &gorm.Config{
|
||||||
Logger: gormLogger,
|
Logger: gormLogger,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dbConfig.Connection == "mysql" {
|
||||||
|
db, err = gorm.Open(mysql.Open(dbPath), c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Connection is sqlite
|
||||||
db, err = gorm.Open(sqlite.Open(dbPath), c)
|
db, err = gorm.Open(sqlite.Open(dbPath), c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := initModels(); err != nil {
|
if err := initModels(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -86,7 +86,7 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||||
|
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
Key string `json:"key" form:"key"`
|
Key string `json:"key" form:"key" gorm:"column:key"`
|
||||||
Value string `json:"value" form:"value"`
|
Value string `json:"value" form:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -22,6 +22,7 @@ require (
|
||||||
golang.org/x/crypto v0.38.0
|
golang.org/x/crypto v0.38.0
|
||||||
golang.org/x/text v0.25.0
|
golang.org/x/text v0.25.0
|
||||||
google.golang.org/grpc v1.72.1
|
google.golang.org/grpc v1.72.1
|
||||||
|
gorm.io/driver/mysql v1.5.7
|
||||||
gorm.io/driver/sqlite v1.5.7
|
gorm.io/driver/sqlite v1.5.7
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
|
@ -41,6 +42,7 @@ require (
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
||||||
|
|
5
go.sum
5
go.sum
|
@ -51,6 +51,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
@ -261,8 +263,11 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||||
|
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||||
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk=
|
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk=
|
||||||
|
|
1
main.go
1
main.go
|
@ -105,6 +105,7 @@ func runWebServer() {
|
||||||
default:
|
default:
|
||||||
server.Stop()
|
server.Stop()
|
||||||
subServer.Stop()
|
subServer.Stop()
|
||||||
|
database.CloseDB()
|
||||||
log.Println("Shutting down servers.")
|
log.Println("Shutting down servers.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,18 +107,30 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
|
||||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var inbounds []*model.Inbound
|
var inbounds []*model.Inbound
|
||||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in (
|
err := db.Model(model.Inbound{}).
|
||||||
SELECT DISTINCT inbounds.id
|
Preload("ClientStats").
|
||||||
FROM inbounds,
|
Where("protocol IN ? AND enable = ?", []string{"vmess", "vless", "trojan", "shadowsocks"}, true).
|
||||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
Find(&inbounds).Error
|
||||||
WHERE
|
|
||||||
protocol in ('vmess','vless','trojan','shadowsocks')
|
|
||||||
AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
|
|
||||||
)`, subId, true).Find(&inbounds).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return inbounds, nil
|
|
||||||
|
// Filter inbounds that have clients with matching subId
|
||||||
|
var filteredInbounds []*model.Inbound
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, err := s.inboundService.GetClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.SubID == subId {
|
||||||
|
filteredInbounds = append(filteredInbounds, inbound)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredInbounds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
|
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
|
||||||
|
@ -132,25 +144,55 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
|
||||||
|
|
||||||
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
|
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var inbound *model.Inbound
|
var inbounds []*model.Inbound
|
||||||
err := db.Model(model.Inbound{}).
|
err := db.Model(model.Inbound{}).Find(&inbounds).Error
|
||||||
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 {
|
if err != nil {
|
||||||
return "", 0, "", err
|
return "", 0, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find inbound with matching fallback dest
|
||||||
|
var masterInbound *model.Inbound
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
var settings map[string]any
|
||||||
|
err := json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbacks, ok := settings["fallbacks"].([]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fallback := range fallbacks {
|
||||||
|
f, ok := fallback.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fallbackDest, ok := f["dest"].(string); ok && fallbackDest == dest {
|
||||||
|
masterInbound = inbound
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if masterInbound != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if masterInbound == nil {
|
||||||
|
return "", 0, "", fmt.Errorf("no inbound found with fallback dest: %s", dest)
|
||||||
|
}
|
||||||
|
|
||||||
var stream map[string]any
|
var stream map[string]any
|
||||||
json.Unmarshal([]byte(streamSettings), &stream)
|
json.Unmarshal([]byte(streamSettings), &stream)
|
||||||
var masterStream map[string]any
|
var masterStream map[string]any
|
||||||
json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
|
json.Unmarshal([]byte(masterInbound.StreamSettings), &masterStream)
|
||||||
stream["security"] = masterStream["security"]
|
stream["security"] = masterStream["security"]
|
||||||
stream["tlsSettings"] = masterStream["tlsSettings"]
|
stream["tlsSettings"] = masterStream["tlsSettings"]
|
||||||
stream["externalProxy"] = masterStream["externalProxy"]
|
stream["externalProxy"] = masterStream["externalProxy"]
|
||||||
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
|
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
|
||||||
|
|
||||||
return inbound.Listen, inbound.Port, string(modifiedStream), nil
|
return masterInbound.Listen, masterInbound.Port, string(modifiedStream), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||||
|
|
|
@ -87,15 +87,24 @@ func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, err
|
||||||
|
|
||||||
func (s *InboundService) getAllEmails() ([]string, error) {
|
func (s *InboundService) getAllEmails() ([]string, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var emails []string
|
var inbounds []*model.Inbound
|
||||||
err := db.Raw(`
|
err := db.Model(model.Inbound{}).Find(&inbounds).Error
|
||||||
SELECT JSON_EXTRACT(client.value, '$.email')
|
|
||||||
FROM inbounds,
|
|
||||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
|
||||||
`).Scan(&emails).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var emails []string
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, err := s.GetClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.Email != "" {
|
||||||
|
emails = append(emails, client.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return emails, nil
|
return emails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1120,14 +1129,46 @@ func (s *InboundService) GetInboundTags() (string, error) {
|
||||||
|
|
||||||
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
|
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
db.Exec(`
|
|
||||||
DELETE FROM client_traffics
|
// Get all inbounds
|
||||||
WHERE email NOT IN (
|
var inbounds []*model.Inbound
|
||||||
SELECT JSON_EXTRACT(client.value, '$.email')
|
err := db.Model(model.Inbound{}).Find(&inbounds).Error
|
||||||
FROM inbounds,
|
if err != nil {
|
||||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
logger.Error("Failed to get inbounds:", err)
|
||||||
)
|
return
|
||||||
`)
|
}
|
||||||
|
|
||||||
|
// Collect all valid emails from inbounds
|
||||||
|
validEmails := make(map[string]bool)
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, err := s.GetClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.Email != "" {
|
||||||
|
validEmails[client.Email] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all client traffics
|
||||||
|
var traffics []xray.ClientTraffic
|
||||||
|
err = db.Model(xray.ClientTraffic{}).Find(&traffics).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to get client traffics:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete traffics with emails not in validEmails
|
||||||
|
for _, traffic := range traffics {
|
||||||
|
if !validEmails[traffic.Email] {
|
||||||
|
err = db.Delete(&traffic).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to delete orphaned traffic:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
|
func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
|
||||||
|
@ -1789,19 +1830,38 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic,
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var traffics []xray.ClientTraffic
|
var traffics []xray.ClientTraffic
|
||||||
|
|
||||||
err := db.Model(xray.ClientTraffic{}).Where(`email IN(
|
// First get all inbounds
|
||||||
SELECT JSON_EXTRACT(client.value, '$.email') as email
|
var inbounds []*model.Inbound
|
||||||
FROM inbounds,
|
err := db.Model(model.Inbound{}).Find(&inbounds).Error
|
||||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
if err != nil {
|
||||||
WHERE
|
return nil, err
|
||||||
JSON_EXTRACT(client.value, '$.id') in (?)
|
}
|
||||||
)`, id).Find(&traffics).Error
|
|
||||||
|
|
||||||
|
// Collect all emails that match the ID
|
||||||
|
var targetEmails []string
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, err := s.GetClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.ID == id && client.Email != "" {
|
||||||
|
targetEmails = append(targetEmails, client.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Get traffics for the collected emails
|
||||||
|
if len(targetEmails) > 0 {
|
||||||
|
err = db.Model(xray.ClientTraffic{}).
|
||||||
|
Where("email IN ?", targetEmails).
|
||||||
|
Find(&traffics).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(err)
|
logger.Debug(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return traffics, err
|
}
|
||||||
|
|
||||||
|
return traffics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
|
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
|
||||||
|
|
|
@ -88,7 +88,7 @@ func (s *SettingService) GetDefaultJsonConfig() (any, error) {
|
||||||
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
|
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
settings := make([]*model.Setting, 0)
|
settings := make([]*model.Setting, 0)
|
||||||
err := db.Model(model.Setting{}).Not("key = ?", "xrayTemplateConfig").Find(&settings).Error
|
err := db.Model(model.Setting{}).Not("`key` = ?", "xrayTemplateConfig").Find(&settings).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -173,7 +173,7 @@ func (s *SettingService) ResetSettings() error {
|
||||||
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
setting := &model.Setting{}
|
setting := &model.Setting{}
|
||||||
err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error
|
err := db.Model(model.Setting{}).Where("`key` = ?", key).First(setting).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue