From 31644dee6b3a1e76d734baf65f0e8e1a0b61f50d Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 30 Mar 2026 19:46:05 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(dameng):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=BE=BE=E6=A2=A6=E6=95=B0=E6=8D=AE=E6=BA=90=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E6=97=A0=E6=B3=95=E5=B1=95=E5=BC=80=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=8A=82=E7=82=B9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 权限适配:取消对 SYSDBA schema 的默认过滤,并增加 `SELECT USER FROM DUAL` 兜底查询 - 树节点容错:Sidebar 当数据库为空时不再阻断加载状态,允许用户重试刷新并增加明确提示 - 类型修正:修复 RedisMonitor 组件 `NodeJS.Timeout` 在 Vite 下的编译报错 - 测试覆盖:补充达梦 SYSDBA 过滤及兜底查询逻辑的单元测试 --- frontend/package.json.md5 | 2 +- frontend/src/components/RedisMonitor.tsx | 2 +- frontend/src/components/Sidebar.tsx | 10 ++++- internal/db/dameng_metadata.go | 8 ++-- internal/db/dameng_metadata_test.go | 47 ++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index b8be944..ad6ce0c 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -f697e821b4acd5cf614d63d46453e8a4 \ No newline at end of file +20168ff7047e0ecea00acb73f413f7db \ No newline at end of file diff --git a/frontend/src/components/RedisMonitor.tsx b/frontend/src/components/RedisMonitor.tsx index 95000ea..0e5cc25 100644 --- a/frontend/src/components/RedisMonitor.tsx +++ b/frontend/src/components/RedisMonitor.tsx @@ -51,7 +51,7 @@ const RedisMonitor: React.FC = ({ connectionId, redisDB }) => // Ref to track if component is mounted to prevent state updates after unmount const mountedRef = useRef(true); // Interval ref - const intervalRef = useRef(null); + const intervalRef = useRef | null>(null); // Previous ops counter to calculate QPS if instantaneous_ops_per_sec is not enough const prevMetricsRef = useRef({ prevOps: 0, prevTime: 0 }); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 64a0232..58658f3 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1036,13 +1036,21 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> dbs = dbs.filter(db => conn.includeDatabases!.includes(db.title)); } - setTreeData(origin => updateTreeData(origin, node.key, dbs)); + if (dbs.length > 0) { + setTreeData(origin => updateTreeData(origin, node.key, dbs)); + } else { + // 空列表:清理 loadedKeys 以允许重新加载,不设置 children = [] + setLoadedKeys(prev => prev.filter(k => k !== node.key)); + message.warning({ content: '未获取到可见数据库/schema,请检查账号权限或右键刷新', key: `conn-${conn.id}-dbs` }); + } } else { setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' })); + setLoadedKeys(prev => prev.filter(k => k !== node.key)); message.error({ content: res.message, key: `conn-${conn.id}-dbs` }); } } catch (e: any) { setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' })); + setLoadedKeys(prev => prev.filter(k => k !== node.key)); message.error({ content: '连接失败: ' + (e?.message || String(e)), key: `conn-${conn.id}-dbs` }); } finally { loadingNodesRef.current.delete(loadKey); diff --git a/internal/db/dameng_metadata.go b/internal/db/dameng_metadata.go index b0f698b..15d12ad 100644 --- a/internal/db/dameng_metadata.go +++ b/internal/db/dameng_metadata.go @@ -9,9 +9,9 @@ import ( ) var damengDatabaseQueries = []string{ - // 优先使用达梦原生系统表 - "SELECT DISTINCT OBJECT_NAME AS DATABASE_NAME FROM SYS.SYSOBJECTS WHERE TYPE$ = 'SCH' AND OBJECT_NAME NOT IN ('SYS','SYSDBA','SYSAUDITOR','SYSSSO','CTISYS','__RECYCLE_USER__') ORDER BY OBJECT_NAME", - "SELECT SCHEMA_NAME AS DATABASE_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('SYS','SYSDBA','SYSAUDITOR','SYSSSO','CTISYS','INFORMATION_SCHEMA') ORDER BY SCHEMA_NAME", + // 优先使用达梦原生系统表(SYSDBA 保留:作为默认管理员 schema,大多数用户在此创建业务表) + "SELECT DISTINCT OBJECT_NAME AS DATABASE_NAME FROM SYS.SYSOBJECTS WHERE TYPE$ = 'SCH' AND OBJECT_NAME NOT IN ('SYS','SYSAUDITOR','SYSSSO','CTISYS','__RECYCLE_USER__') ORDER BY OBJECT_NAME", + "SELECT SCHEMA_NAME AS DATABASE_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('SYS','SYSAUDITOR','SYSSSO','CTISYS','INFORMATION_SCHEMA') ORDER BY SCHEMA_NAME", // Oracle 兼容层 "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS DATABASE_NAME FROM DUAL", "SELECT SYS_CONTEXT('USERENV', 'CURRENT_USER') AS DATABASE_NAME FROM DUAL", @@ -21,6 +21,8 @@ var damengDatabaseQueries = []string{ "SELECT USERNAME AS DATABASE_NAME FROM SYS.DBA_USERS ORDER BY USERNAME", "SELECT DISTINCT OWNER AS DATABASE_NAME FROM ALL_OBJECTS ORDER BY OWNER", "SELECT DISTINCT OWNER AS DATABASE_NAME FROM ALL_TABLES ORDER BY OWNER", + // 最终兜底:获取当前连接用户作为 schema 名称 + "SELECT USER AS DATABASE_NAME FROM DUAL", } type damengQueryFunc func(query string) ([]map[string]interface{}, []string, error) diff --git a/internal/db/dameng_metadata_test.go b/internal/db/dameng_metadata_test.go index 5310679..b5b6557 100644 --- a/internal/db/dameng_metadata_test.go +++ b/internal/db/dameng_metadata_test.go @@ -71,3 +71,50 @@ func TestCollectDamengDatabaseNames_ReturnsErrorWhenNoNameResolved(t *testing.T) t.Fatalf("错误不符合预期: %v", err) } } + +// TestCollectDamengDatabaseNames_IncludesSYSDBA 验证 SYSDBA(达梦默认管理员 schema) +// 不会被系统 schema 过滤排除。 +func TestCollectDamengDatabaseNames_IncludesSYSDBA(t *testing.T) { + t.Parallel() + + got, err := collectDamengDatabaseNames(func(query string) ([]map[string]interface{}, []string, error) { + switch query { + case damengDatabaseQueries[0]: + // 查询 0 返回 SYSDBA(之前会被排除,修复后应该返回) + return []map[string]interface{}{{"DATABASE_NAME": "SYSDBA"}}, nil, nil + default: + return nil, nil, errors.New("permission denied") + } + }) + if err != nil { + t.Fatalf("collectDamengDatabaseNames 返回错误: %v", err) + } + + want := []string{"SYSDBA"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("SYSDBA 应该包含在结果中, got=%v want=%v", got, want) + } +} + +// TestCollectDamengDatabaseNames_FallbackToCurrentUser 验证当所有查询都失败时 +// 兜底查询 SELECT USER FROM DUAL 能返回当前用户作为 schema。 +func TestCollectDamengDatabaseNames_FallbackToCurrentUser(t *testing.T) { + t.Parallel() + + lastQuery := damengDatabaseQueries[len(damengDatabaseQueries)-1] + got, err := collectDamengDatabaseNames(func(query string) ([]map[string]interface{}, []string, error) { + if query == lastQuery { + return []map[string]interface{}{{"DATABASE_NAME": "SYSDBA"}}, nil, nil + } + // 前面所有查询要么返回空要么报错 + return []map[string]interface{}{}, nil, nil + }) + if err != nil { + t.Fatalf("collectDamengDatabaseNames 返回错误: %v", err) + } + + want := []string{"SYSDBA"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("兜底查询应该返回当前用户, got=%v want=%v", got, want) + } +}