docs: add MariaDB support design spec

Design for adding MariaDB as alternative database backend with
data migration, x-ui.sh switching UI, and driver-agnostic InitDB.
This commit is contained in:
Sora39831 2026-04-03 09:14:20 +08:00
parent 09f84782b0
commit 82a1b85d45

View file

@ -0,0 +1,332 @@
# MariaDB Support for 3x-ui
## Summary
Add MariaDB as an alternative database backend to SQLite. Users switch between SQLite and MariaDB via the `x-ui.sh` management script (option 27). Data is automatically migrated during the switch. MariaDB connection credentials are stored in `/etc/x-ui/x-ui.json`.
## Requirements
- Support both SQLite and MariaDB as database backends
- Switch via `x-ui.sh` with interactive prompts for MariaDB credentials (IP, port, username, password, database name)
- Auto-migrate data when switching between SQLite and MariaDB
- Keep old database as backup after migration
- MariaDB has core feature parity (CRUD, migrations, seeders) but skips SQLite-specific features (WAL checkpoint, file export/import)
- Credentials stored in `/etc/x-ui/x-ui.json`
## Architecture: Approach A — Driver-agnostic `InitDB`
Refactor `database.InitDB()` to read config from the JSON settings file, determine the driver type, and open the appropriate GORM connection. The package-level `var db *gorm.DB` singleton stays unchanged — all callers continue using `database.GetDB()`.
---
## Section 1: Configuration
### New settings in `web/service/setting.go`
Add to `defaultValueMap`:
| Key | Default | Description |
|-----|---------|-------------|
| `dbType` | `"sqlite"` | `"sqlite"` or `"mariadb"` |
| `dbHost` | `"127.0.0.1"` | MariaDB host |
| `dbPort` | `"3306"` | MariaDB port |
| `dbUser` | `""` | MariaDB username |
| `dbPassword` | `""` | MariaDB password |
| `dbName` | `"3xui"` | MariaDB database name |
Add getter/setter methods: `GetDBType()`, `SetDBType()`, `GetDBHost()`, `SetDBHost()`, `GetDBPort()`, `SetDBPort()`, `GetDBUser()`, `SetDBUser()`, `GetDBPassword()`, `SetDBPassword()`, `GetDBName()`, `SetDBName()`.
### Config reading before DB init
Problem: settings are stored IN the database, but we need `dbType` BEFORE opening the DB.
Solution: `config/config.go` gets a `GetDBTypeFromJSON()` function that reads `/etc/x-ui/x-ui.json` directly (falls back to `"sqlite"` if file doesn't exist or key is missing). This is called before `database.InitDB()`.
### New CLI flags in `main.go`
Add `-dbType`, `-dbHost`, `-dbPort`, `-dbUser`, `-dbPassword`, `-dbName` flags to the `setting` subcommand. These write directly to the JSON config file (not via the DB) using `config.WriteSettingToJSON(key, value)`.
New `config/config.go` helper: `WriteSettingToJSON(key, value string)` — reads the JSON file, updates the key, writes back.
---
## Section 2: Database Layer (`database/db.go`)
### Refactored `InitDB()`
```go
func InitDB() error {
dbType := config.GetDBTypeFromJSON()
switch dbType {
case "mariadb":
return initMariaDB()
default: // "sqlite"
return initSQLite(config.GetDBPath())
}
}
```
### `initSQLite(path string) error`
Existing logic unchanged — opens SQLite with `gorm.io/driver/sqlite`, runs `initModels()`, `initUser()`, `runSeeders()`.
### `initMariaDB() error`
1. Read host, port, user, password, dbName from JSON config.
2. Build DSN: `user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local`
3. Open with `gorm.io/driver/mysql`.
4. Run `initModels()`, `initUser()`, `runSeeders()` (same as SQLite).
### Adapted functions
- `Checkpoint()` — if MariaDB, return `nil`. If SQLite, existing WAL logic.
- `IsSQLiteDB()` — unchanged, only called for SQLite.
- `ValidateSQLiteDB()` — unchanged, only called for SQLite.
### New dependency
`gorm.io/driver/mysql` added to `go.mod`.
---
## Section 3: Data Migration (`database/migrate.go`)
New file with two functions:
### `MigrateSQLiteToMariaDB() error`
1. Open SQLite connection from `config.GetDBPath()`.
2. Open MariaDB connection from JSON settings.
3. For each table (users, inbounds, outbound_traffics, settings, inbound_client_ips, client_traffics, history_of_seeders):
- AutoMigrate the model on MariaDB.
- `SELECT *` from SQLite → `INSERT` into MariaDB using GORM raw SQL.
4. On success: close connections (SQLite file kept as backup).
5. On failure: return error with context.
### `MigrateMariaDBToSQLite() error`
Reverse of above:
1. Open MariaDB connection from JSON settings.
2. Open/create SQLite connection at `config.GetDBPath()`.
3. For each table: read from MariaDB, write to SQLite.
4. On success: close connections.
5. On failure: return error.
Row transfer approach: Use the existing model structs explicitly. For each table, query all rows from source into a `[]Model` slice, then batch-insert into destination. This avoids raw SQL differences between SQLite and MySQL. Example for users:
```go
var users []model.User
srcDB.Find(&users)
dstDB.CreateInBatches(&users, 100)
```
This pattern repeats for each of the 7 tables.
---
## Section 4: `main.go` Changes
### Updated callers
All `database.InitDB(config.GetDBPath())` calls change to `database.InitDB()`:
- `runWebServer()` (line 49)
- `resetSetting()` (line 134)
- `updateTgbotSetting()` (line 221)
- `updateSetting()` (line 259)
- `updateCert()` (line 318)
- `migrateDb()` (line 395)
### New `migrate-db` subcommand
```go
case "migrate-db":
migrateDbBetweenDrivers()
```
`migrateDbBetweenDrivers()`:
1. Read `dbType` from JSON config.
2. If `dbType == "mariadb"`: call `database.MigrateSQLiteToMariaDB()`.
3. If `dbType == "sqlite"`: call `database.MigrateMariaDBToSQLite()`.
4. Print success/failure message.
### New CLI flags
Add to `setting` subcommand:
- `-dbType string` — set database type
- `-dbHost string` — set MariaDB host
- `-dbPort string` — set MariaDB port
- `-dbUser string` — set MariaDB username
- `-dbPassword string` — set MariaDB password
- `-dbName string` — set MariaDB database name
These call `config.WriteSettingToJSON()` to write directly to the JSON file. Only the 6 DB-related settings use `WriteSettingToJSON()` — all other settings (port, username, etc.) continue to use the existing `SettingService` methods that write through the database.
---
## Section 5: `web/service/server.go` Changes
### `GetDb()`
Add check at the top:
```go
dbType, _ := s.GetDBType()
if dbType == "mariadb" {
return nil, common.NewError("Database export is not supported for MariaDB")
}
```
Existing SQLite logic unchanged.
### `ImportDB()`
Add check at the top:
```go
dbType, _ := s.GetDBType()
if dbType == "mariadb" {
return common.NewError("Database import is not supported for MariaDB")
}
```
Existing SQLite logic unchanged.
---
## Section 6: `x-ui.sh` Changes
### New menu option 27
Add to `show_menu`:
```
│────────────────────────────────────────────────│
│ ${green}27.${plain} 数据库管理 │
```
Add to the case statement:
```bash
27)
check_install && db_menu
;;
```
Update prompt: `请输入选择 [0-27]`
### `db_menu()` function
```bash
db_menu() {
# Read current dbType from JSON
local current_type=$(read_json_dbtype)
echo -e "
╔────────────────────────────────────────────────╗
│ ${green}数据库管理${plain} │
│ ${green}0.${plain} 返回主菜单 │
│ ${green}1.${plain} 查看当前数据库类型(当前: ${current_type}
│ ${green}2.${plain} 切换到 MariaDB │
│ ${green}3.${plain} 切换到 SQLite │
╚────────────────────────────────────────────────╝
"
read -rp "请输入选择 [0-3]" num
case "${num}" in
0) show_menu ;;
1) db_show_status && db_menu ;;
2) db_switch_to_mariadb ;;
3) db_switch_to_sqlite ;;
*) echo "无效选项" && db_menu ;;
esac
}
```
### `db_switch_to_mariadb()`
```bash
db_switch_to_mariadb() {
echo "请输入 MariaDB 连接信息(直接回车使用默认值):"
read -rp "MariaDB IP默认 127.0.0.1: " db_host
db_host=${db_host:-127.0.0.1}
read -rp "MariaDB 端口(默认 3306: " db_port
db_port=${db_port:-3306}
read -rp "MariaDB 用户名: " db_user
if [ -z "$db_user" ]; then
echo -e "${red}用户名不能为空${plain}"
db_menu
return
fi
read -rsp "MariaDB 密码: " db_pass
echo
if [ -z "$db_pass" ]; then
echo -e "${red}密码不能为空${plain}"
db_menu
return
fi
read -rp "数据库名(默认 3xui: " db_name
db_name=${db_name:-3xui}
# Write settings to JSON config
/usr/local/x-ui/x-ui setting -dbType mariadb -dbHost "$db_host" -dbPort "$db_port" -dbUser "$db_user" -dbPassword "$db_pass" -dbName "$db_name"
# Migrate data
echo "正在迁移数据从 SQLite 到 MariaDB..."
/usr/local/x-ui/x-ui migrate-db
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
restart
else
echo -e "${red}数据迁移失败,正在回滚到 SQLite...${plain}"
/usr/local/x-ui/x-ui setting -dbType sqlite
restart
fi
}
```
### `db_switch_to_sqlite()`
```bash
db_switch_to_sqlite() {
/usr/local/x-ui/x-ui setting -dbType sqlite
echo "正在迁移数据从 MariaDB 到 SQLite..."
/usr/local/x-ui/x-ui migrate-db
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
restart
else
echo -e "${red}数据迁移失败${plain}"
fi
}
```
### Helper functions in x-ui.sh
- `read_json_dbtype()` — reads `dbType` from `/etc/x-ui/x-ui.json` using `grep`/`sed` or Python if available.
- `db_show_status()` — displays current DB type and connection info.
---
## Files Changed
| File | Changes |
|------|---------|
| `go.mod` | Add `gorm.io/driver/mysql` |
| `config/config.go` | Add `GetDBTypeFromJSON()`, `WriteSettingToJSON()` |
| `database/db.go` | Refactor `InitDB()` to be driver-agnostic, add `initMariaDB()`, adapt `Checkpoint()` |
| `database/migrate.go` | **New file**`MigrateSQLiteToMariaDB()`, `MigrateMariaDBToSQLite()` |
| `main.go` | Update all `InitDB` calls, add `migrate-db` subcommand, add setting CLI flags |
| `web/service/setting.go` | Add 6 new settings + getter/setter methods |
| `web/service/server.go` | Guard `GetDb()`/`ImportDB()` for MariaDB |
| `x-ui.sh` | Add option 27, `db_menu()`, `db_switch_to_mariadb()`, `db_switch_to_sqlite()`, helpers |
## Testing
1. Fresh install with SQLite (default) — verify panel works as before
2. Switch to MariaDB via x-ui.sh — verify data migrates and panel starts
3. Switch back to SQLite — verify data migrates back
4. Verify MariaDB CRUD operations (create inbound, modify settings, etc.)
5. Verify GetDb/ImportDB return appropriate errors when using MariaDB
6. Verify invalid MariaDB credentials show error and rollback to SQLite