feat: Add MySQL database support (#3024)

* 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.

* refactor: prefix env variables of database with XUI_ to support direct environment usage without .env file

All database configuration environment variables now start with the XUI_ prefix to avoid conflicts and allow configuration via system-level environment variables, not just the .env file.
This commit is contained in:
Ali Golzar
2025-05-21 13:34:38 +03:30
committed by GitHub
parent 1b1cbfff42
commit 3850e2f070
10 changed files with 239 additions and 52 deletions

View File

@@ -87,15 +87,24 @@ 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
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Find(&inbounds).Error
if err != nil {
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
}
@@ -1120,14 +1129,46 @@ 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
)
`)
// Get all inbounds
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Find(&inbounds).Error
if err != nil {
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 {
@@ -1789,19 +1830,38 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic,
db := database.GetDB()
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
// First get all inbounds
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Find(&inbounds).Error
if err != nil {
logger.Debug(err)
return nil, err
}
return traffics, err
// 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 {
logger.Debug(err)
return nil, err
}
}
return traffics, nil
}
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {

View File

@@ -88,7 +88,7 @@ func (s *SettingService) GetDefaultJsonConfig() (any, error) {
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
db := database.GetDB()
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 {
return nil, err
}
@@ -173,7 +173,7 @@ func (s *SettingService) ResetSettings() error {
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
db := database.GetDB()
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 {
return nil, err
}