🔧 fix(connection-modal): 修复多数据源URI导入解析并校正Oracle服务名校验

- 新增单主机URI解析映射,兼容 postgres/postgresql、sqlserver、redis、tdengine、dameng(dm)、kingbase、highgo、vastbase、clickhouse、oracle
- 抽取 parseSingleHostUri 复用逻辑,统一 host/port/user/password/database 回填行为
- Oracle 连接新增服务名必填校验,移除“服务名为空回退用户名”的隐式逻辑
- 连接弹窗补充 Oracle 服务名输入项与 URI 示例
This commit is contained in:
Syngnat
2026-03-02 11:46:59 +08:00
parent 4d0940636d
commit 84688e995a
2 changed files with 75 additions and 18 deletions

View File

@@ -41,6 +41,19 @@ const getDefaultPortByType = (type: string) => {
}
};
const singleHostUriSchemesByType: Record<string, string[]> = {
postgres: ['postgresql', 'postgres'],
clickhouse: ['clickhouse'],
oracle: ['oracle'],
sqlserver: ['sqlserver'],
redis: ['redis'],
tdengine: ['tdengine'],
dameng: ['dameng', 'dm'],
kingbase: ['kingbase'],
highgo: ['highgo'],
vastbase: ['vastbase'],
};
const isFileDatabaseType = (type: string) => type === 'sqlite' || type === 'duckdb';
type DriverStatusSnapshot = {
@@ -344,6 +357,41 @@ const ConnectionModal: React.FC<{
};
};
const parseSingleHostUri = (
uriText: string,
expectedSchemes: string[],
defaultPort: number,
): { host: string; port: number; username: string; password: string; database: string } | null => {
let parsed: ReturnType<typeof parseMultiHostUri> | null = null;
for (const scheme of expectedSchemes) {
parsed = parseMultiHostUri(uriText, scheme);
if (parsed) {
break;
}
}
if (!parsed) {
return null;
}
if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) {
return null;
}
if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) {
return null;
}
const hostList = normalizeAddressList(parsed.hosts, defaultPort);
if (!hostList.length) {
return null;
}
const primary = parseHostPort(hostList[0] || `localhost:${defaultPort}`, defaultPort);
return {
host: primary?.host || 'localhost',
port: primary?.port || defaultPort,
username: parsed.username,
password: parsed.password,
database: parsed.database || '',
};
};
const parseUriToValues = (uriText: string, type: string): Record<string, any> | null => {
const trimmedUri = String(uriText || '').trim();
if (!trimmedUri) {
@@ -441,28 +489,22 @@ const ConnectionModal: React.FC<{
};
}
if (type === 'clickhouse') {
const parsed = parseMultiHostUri(trimmedUri, 'clickhouse');
const singleHostSchemes = singleHostUriSchemesByType[type];
if (singleHostSchemes && singleHostSchemes.length > 0) {
const parsed = parseSingleHostUri(trimmedUri, singleHostSchemes, getDefaultPortByType(type));
if (!parsed) {
return null;
}
if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) {
if (type === 'oracle' && !String(parsed.database || '').trim()) {
// Oracle 需要显式 service name避免 URI 解析后放过必填校验。
return null;
}
if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) {
return null;
}
const hostList = normalizeAddressList(parsed.hosts, 9000);
if (!hostList.length) {
return null;
}
const primary = parseHostPort(hostList[0] || 'localhost:9000', 9000);
return {
host: primary?.host || 'localhost',
port: primary?.port || 9000,
host: parsed.host,
port: parsed.port,
user: parsed.username,
password: parsed.password,
database: parsed.database || '',
database: parsed.database,
};
}
@@ -503,6 +545,9 @@ const ConnectionModal: React.FC<{
if (dbType === 'clickhouse') {
return 'clickhouse://default:pass@127.0.0.1:9000/default';
}
if (dbType === 'oracle') {
return 'oracle://user:pass@127.0.0.1:1521/ORCLPDB1';
}
return '例如: postgres://user:pass@127.0.0.1:5432/db_name';
};
@@ -1446,6 +1491,17 @@ const ConnectionModal: React.FC<{
</Form.Item>
)}
{dbType === 'oracle' && (
<Form.Item
name="database"
label="服务名 (Service Name)"
rules={[createUriAwareRequiredRule('请输入 Oracle 服务名(例如 ORCLPDB1')]}
help="请填写监听器注册的 SERVICE_NAME不是用户名。例如ORCLPDB1"
>
<Input placeholder="例如ORCLPDB1" />
</Form.Item>
)}
{(dbType === 'mysql' || dbType === 'mariadb' || dbType === 'diros' || dbType === 'sphinx') && (
<>
<Form.Item name="mysqlTopology" label="连接模式">

View File

@@ -26,10 +26,7 @@ type OracleDB struct {
func (o *OracleDB) getDSN(config connection.ConnectionConfig) string {
// oracle://user:pass@host:port/service_name
database := config.Database
if database == "" {
database = config.User // Default to user service/schema if empty?
}
database := strings.TrimSpace(config.Database)
u := &url.URL{
Scheme: "oracle",
@@ -44,6 +41,10 @@ func (o *OracleDB) getDSN(config connection.ConnectionConfig) string {
func (o *OracleDB) Connect(config connection.ConnectionConfig) error {
var dsn string
var err error
serviceName := strings.TrimSpace(config.Database)
if serviceName == "" {
return fmt.Errorf("Oracle 连接缺少服务名Service Name请在连接配置中填写例如 ORCLPDB1")
}
if config.UseSSH {
// Create SSH tunnel with local port forwarding