mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 07:56:37 +08:00
feat: add comprehensive migration test for legacy columns
- Add ExtraIPs, TCPListenAddr, UDPListenAddr to Node migration - Add ip_preference to Tunnel migration - Add test for very legacy database migration (1.x schema) - Include issue #211 tracking document Entire-Checkpoint: 0d086883c34a
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
# Issue #211: 转发自定义监听IP / 隧道指定连接IP
|
||||||
|
|
||||||
|
## 需求总结
|
||||||
|
1. **节点**: 高级配置增加"额外IP地址"字段(逗号分隔)
|
||||||
|
2. **转发**: 创建/编辑时可指定入口监听IP
|
||||||
|
3. **隧道**: 配置出口节点时可指定连接IP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务清单
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- [x] 1. 数据模型扩展 - Node/ForwardPort/ChainTunnel 增加字段
|
||||||
|
- [x] 2. Repository - CreateNode/UpdateNode 处理 extraIPs
|
||||||
|
- [x] 3. Repository - resolveForwardIngress 使用 forward_port.in_ip
|
||||||
|
- [x] 4. Repository - GetNodeAllIPs 辅助函数(返回节点所有可用IP)
|
||||||
|
- [x] 5. Handler - 转发创建/更新处理 inIp 参数
|
||||||
|
- [x] 6. Handler - 隧道出口节点处理 connectIp 参数
|
||||||
|
- [x] 7. Handler - 节点API返回 extraIPs 字段
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- [x] 8. 节点编辑页 - 高级配置增加"额外IP"输入
|
||||||
|
- [x] 9. 转发编辑弹窗 - 增加"监听IP"下拉选择
|
||||||
|
- [x] 10. 隧道配置页 - 出口节点增加"连接IP"输入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完成进度
|
||||||
|
- 开始时间: 2026-03-02
|
||||||
|
- 完成时间: 2026-03-02
|
||||||
|
- 完成任务: 10/10
|
||||||
|
- 后端完成: ✅
|
||||||
|
- 前端完成: ✅
|
||||||
@@ -260,7 +260,7 @@ func prepareSQLiteLegacyColumns(db *gorm.DB) error {
|
|||||||
m := db.Migrator()
|
m := db.Migrator()
|
||||||
|
|
||||||
if m.HasTable(&model.Node{}) {
|
if m.HasTable(&model.Node{}) {
|
||||||
for _, field := range []string{"ServerIPV4", "ServerIPV6", "Inx", "IsRemote", "RemoteURL", "RemoteToken", "RemoteConfig"} {
|
for _, field := range []string{"ServerIPV4", "ServerIPV6", "ExtraIPs", "TCPListenAddr", "UDPListenAddr", "Inx", "IsRemote", "RemoteURL", "RemoteToken", "RemoteConfig"} {
|
||||||
if m.HasColumn(&model.Node{}, field) {
|
if m.HasColumn(&model.Node{}, field) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -684,15 +684,107 @@ func TestOpenMigratesLegacyNodeDualStackColumns(t *testing.T) {
|
|||||||
|
|
||||||
columns := readTableColumns(t, r.DB(), "node")
|
columns := readTableColumns(t, r.DB(), "node")
|
||||||
|
|
||||||
for _, required := range []string{"server_ip_v4", "server_ip_v6", "inx"} {
|
for _, required := range []string{"server_ip_v4", "server_ip_v6", "inx", "extra_ips"} {
|
||||||
if !columns[required] {
|
if !columns[required] {
|
||||||
t.Fatalf("expected node column %q to exist after migration", required)
|
t.Fatalf("expected node column %q to exist after migration", required)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnelColumns := readTableColumns(t, r.DB(), "tunnel")
|
tunnelColumns := readTableColumns(t, r.DB(), "tunnel")
|
||||||
if !tunnelColumns["inx"] {
|
for _, required := range []string{"inx", "ip_preference"} {
|
||||||
t.Fatalf("expected tunnel column %q to exist after migration", "inx")
|
if !tunnelColumns[required] {
|
||||||
|
t.Fatalf("expected tunnel column %q to exist after migration", required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenMigratesVeryLegacyNodeAndTunnelColumns(t *testing.T) {
|
||||||
|
dbPath := filepath.Join(t.TempDir(), "legacy-1.x.db")
|
||||||
|
legacyDB, err := sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open legacy sqlite: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = legacyDB.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := legacyDB.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS node (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
secret VARCHAR(100) NOT NULL,
|
||||||
|
server_ip VARCHAR(100) NOT NULL,
|
||||||
|
port TEXT NOT NULL,
|
||||||
|
interface_name VARCHAR(200),
|
||||||
|
version VARCHAR(100),
|
||||||
|
http INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tls INTEGER NOT NULL DEFAULT 0,
|
||||||
|
socks INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_time INTEGER NOT NULL,
|
||||||
|
updated_time INTEGER,
|
||||||
|
status INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatalf("create very legacy node table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := legacyDB.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS tunnel (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
traffic_ratio REAL NOT NULL DEFAULT 1.0,
|
||||||
|
type INTEGER NOT NULL,
|
||||||
|
protocol VARCHAR(10) NOT NULL DEFAULT 'tls',
|
||||||
|
flow INTEGER NOT NULL,
|
||||||
|
created_time INTEGER NOT NULL,
|
||||||
|
updated_time INTEGER NOT NULL,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
in_ip TEXT
|
||||||
|
)
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatalf("create very legacy tunnel table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
if _, err := legacyDB.Exec(`
|
||||||
|
INSERT INTO node(name, secret, server_ip, port, interface_name, version, http, tls, socks, created_time, updated_time, status)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, "legacy-node", "legacy-secret", "10.10.0.1", "10000-10010", "eth0", "v-old", 1, 1, 1, now, now, 1); err != nil {
|
||||||
|
t.Fatalf("seed legacy node row: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := repo.Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open migrated sqlite: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = r.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
columns := readTableColumns(t, r.DB(), "node")
|
||||||
|
for _, required := range []string{
|
||||||
|
"server_ip_v4",
|
||||||
|
"server_ip_v6",
|
||||||
|
"extra_ips",
|
||||||
|
"tcp_listen_addr",
|
||||||
|
"udp_listen_addr",
|
||||||
|
"inx",
|
||||||
|
"is_remote",
|
||||||
|
"remote_url",
|
||||||
|
"remote_token",
|
||||||
|
"remote_config",
|
||||||
|
} {
|
||||||
|
if !columns[required] {
|
||||||
|
t.Fatalf("expected node column %q to exist after migration", required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelColumns := readTableColumns(t, r.DB(), "tunnel")
|
||||||
|
for _, required := range []string{"inx", "ip_preference"} {
|
||||||
|
if !tunnelColumns[required] {
|
||||||
|
t.Fatalf("expected tunnel column %q to exist after migration", required)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user