Commit 6edeb386 authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 实际数据库连接(参考 web2)

- 后端 core/ 模块自包含(拷贝 web2 的 db_adapter.py / sql_loader.py / models.py + 8 个 SQL 模板),不依赖 web.core
- requirements.txt 加 pymysql / oracledb / dmPython
- routers/db.py 新增 /api/connect/test + /api/connect/schemas + /api/connect/tables 三个端点
- app.py 注册 db_router
- 前端 api/connections.js 封装 3 个调用 + 中文 label→小写 db_type 转换
- TaskModal.vue testConn() / loadSchemas() / loadTables() 改真实后端调用,去掉所有 mock
parent 11d7089d
...@@ -50,11 +50,15 @@ app.add_middleware( ...@@ -50,11 +50,15 @@ app.add_middleware(
) )
from web3.backend.routers.connection_presets import router as connection_presets_router # noqa: E402
from web3.backend.routers.db import router as db_router # noqa: E402
from web3.backend.routers.task_groups import router as task_groups_router # noqa: E402 from web3.backend.routers.task_groups import router as task_groups_router # noqa: E402
from web3.backend.routers.tasks import router as tasks_router # noqa: E402 from web3.backend.routers.tasks import router as tasks_router # noqa: E402
app.include_router(task_groups_router, prefix="/api") app.include_router(task_groups_router, prefix="/api")
app.include_router(tasks_router, prefix="/api") app.include_router(tasks_router, prefix="/api")
logger.info("已注册路由:/api/task-groups, /api/tasks") app.include_router(db_router, prefix="/api")
app.include_router(connection_presets_router, prefix="/api")
logger.info("已注册路由:/api/task-groups, /api/tasks, /api/connect/*, /api/connection-presets")
@app.get("/api/health") @app.get("/api/health")
......
This diff is collapsed.
"""web3 数据库连接 Pydantic 模型
来源:精简自 web/core/models.py(2026-08-20),只保留数据库连接所需的三个类。
"""
from __future__ import annotations
from typing import Optional
from pydantic import BaseModel, Field
# ── 请求 ────────────────────────────────────────────────
class ConnectRequest(BaseModel):
"""统一的数据库连接请求基类(测试连接 + 列出 schema/表 共用)"""
db_type: str = Field(..., description="mysql / dameng / oracle")
host: str
port: int = 3306
user: str
password: str
database: str
charset: str = "utf8mb4"
connect_timeout: int = 10
# Oracle 专用:Oracle Instant Client 的 lib_dir(oci.dll 所在目录)
oracle_client_dir: Optional[str] = None
class TestConnectionRequest(ConnectRequest):
"""只测连通性,不启动任务;前端不需要填 tables(会被忽略)"""
tables: list[str] = Field(default_factory=list)
# ── 响应 ────────────────────────────────────────────────
class TestConnectionResponse(BaseModel):
ok: bool
message: str
db_type: Optional[str] = None
database_name: Optional[str] = None # 改名为避免与 BaseModel.schema() 冲突
tables_count: Optional[int] = None
table_summary: list[dict] = Field(default_factory=list)
data_dictionary: list[dict] = Field(default_factory=list)
\ No newline at end of file
"""SQL 模板加载器(web3 自包含版)
来源:精简自 web/sql/loader.py(2026-08-20)。
变化点:
- 模板根目录改为 web3/backend/core/sql_templates/
- 移除 `${var | quote}` filter(web3 用法不需要;quote 由调用方在 Python 侧处理)
- 移除对 web.core.db_adapter.quote_ident 的依赖
把所有 SQL 语句集中到 web3/backend/core/sql_templates/ 目录下,Python 代码不再直接拼 SQL。
模板使用 ${var} 占位符(避免与 SQL 自身的 {} 冲突)。
支持的占位符:
- ${var_name} 简单字符串替换
- ${list | join:","} 列表拼接(默认分隔符 ",")
- ${list | join:" AND "} 自定义分隔符
Dialect 加载规则:
- 优先加载 <name>.<dialect>.sql
- 回退到 <name>.sql(通用版本)
- 都没有则报错
示例:
loader = get_sql_loader()
sql = loader.render("info_schema/list_tables", dialect="mysql", schema="mydb")
"""
from __future__ import annotations
import logging
import re
from pathlib import Path
from threading import Lock
from typing import Any
logger = logging.getLogger(__name__)
# ── 占位符语法 ────────────────────────────────────────────
# 支持:${var} / ${var | filter} / ${var | filter:"sep"}
# 不支持嵌套 ${${a}} 这种(SQL 模板里不需要)
_PLACEHOLDER_RE = re.compile(
r"\$\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*"
r"(?:\|\s*([A-Za-z_]+)\s*(?::\s*\"([^\"]*)\")?\s*)?\}"
)
def _render_placeholders(template: str, params: dict) -> str:
"""把 ${var} / ${var | filter} 替换为 params 里的值
内置 filter:
- join[:"<sep>"]: 把列表按 sep 拼接,默认 ","
- upper: 转大写
- lower: 转小写
"""
def _replace(m: re.Match) -> str:
var_name = m.group(1)
filt = m.group(2)
sep = m.group(3) # filter 的字符串参数
if var_name not in params:
raise KeyError(
f"SQL 模板参数缺失: {var_name!r}(提供: {list(params.keys())})"
)
value = params[var_name]
# 应用 filter
if filt == "join":
if not isinstance(value, (list, tuple)):
raise TypeError(f"join filter 需要 list/tuple,{var_name} 是 {type(value).__name__}")
return sep.join(str(v) for v in value)
elif filt == "upper":
return str(value).upper()
elif filt == "lower":
return str(value).lower()
elif filt is None:
return str(value)
else:
raise ValueError(f"未知 filter: {filt!r}({var_name})")
return _PLACEHOLDER_RE.sub(_replace, template)
# ── 加载器 ────────────────────────────────────────────────
class SQLLoader:
"""SQL 模板加载器(单例)"""
def __init__(self, base_dir: Path | None = None):
# web3/backend/core/sql_loader.py → web3/backend/core/sql_templates/ 是模板根目录
self.base_dir = base_dir or (Path(__file__).resolve().parent / "sql_templates")
self._cache: dict[str, str] = {}
self._lock = Lock()
# ── 加载 ──
def _find_template_file(self, name: str, dialect: str) -> Path:
"""优先找 .<dialect>.sql,回退 .sql"""
dsp = self.base_dir / f"{name}.{dialect}.sql"
if dsp.exists():
return dsp
common = self.base_dir / f"{name}.sql"
if common.exists():
return common
raise FileNotFoundError(
f"SQL 模板不存在: {name}(dialect={dialect});"
f"已查找: {dsp.relative_to(self.base_dir)} 和 {common.relative_to(self.base_dir)}"
)
def _load_text(self, name: str, dialect: str) -> str:
cache_key = f"{dialect}::{name}"
with self._lock:
if cache_key in self._cache:
return self._cache[cache_key]
path = self._find_template_file(name, dialect)
text = path.read_text(encoding="utf-8")
self._cache[cache_key] = text
logger.debug(f"加载 SQL 模板: {path.relative_to(self.base_dir)}")
return text
# ── 渲染 ──
def render(self, name: str, dialect: str = "mysql", **params: Any) -> str:
"""渲染 SQL 模板
Args:
name: 模板路径(不含 .sql 后缀),如 "info_schema/list_tables"
dialect: 数据库方言 "mysql" / "dameng" / "oracle"
**params: 占位符参数
Returns:
渲染后的 SQL 字符串
"""
text = self._load_text(name, dialect)
try:
return _render_placeholders(text, params)
except KeyError as e:
raise KeyError(f"SQL 模板 {name!r} (dialect={dialect}): {e}") from None
# ── 清缓存(热加载调试用) ──
def clear_cache(self) -> None:
with self._lock:
self._cache.clear()
# ── 全局单例 ──────────────────────────────────────────────
_instance: SQLLoader | None = None
def get_sql_loader() -> SQLLoader:
"""获取 SQL 加载器单例"""
global _instance
if _instance is None:
_instance = SQLLoader()
return _instance
def reset_sql_loader() -> None:
"""重置(测试用)"""
global _instance
_instance = None
\ No newline at end of file
-- ============================================================================
-- 健康检查:确认连接可用,返回固定值 1
-- 调用方:web/core/db_adapter.py → test_connection()
-- 方言:Oracle
--
-- Oracle 严格遵循 SQL 标准 —— SELECT 必须有 FROM 子句,
-- 因此不能像 MySQL 那样写 SELECT 1,必须 FROM dual。
-- dual 是 Oracle 内置的单行单列表,恒定返回 1 行。
-- ============================================================================
SELECT 1 FROM dual
\ No newline at end of file
-- ============================================================================
-- 健康检查:确认连接可用,返回固定值 1
-- 调用方:web/core/db_adapter.py → test_connection()
-- 跨方言通用
-- ============================================================================
SELECT 1
\ No newline at end of file
-- ============================================================================
-- 列出指定 schema 下所有列的元数据 (达梦方言)
-- 调用方:web/core/db_adapter.py → list_columns()
-- 参数:schema 数据库/模式名(绑定参数,由调用方通过 params 传入,勿拼接)
-- 说明:
-- - 达梦的 ALL_TAB_COLUMNS.COMMENTS 列名与 MySQL 的 COLUMN_COMMENT 不同
-- - 达梦的 OWNER 通常为大写
-- - 列注释不在 ALL_TAB_COLUMNS 里,要 LEFT JOIN ALL_COL_COMMENTS(Oracle 同源)
-- ============================================================================
SELECT
c.TABLE_NAME AS table_name,
c.COLUMN_NAME AS column_name,
c.COLUMN_ID AS ordinal_position,
c.DATA_TYPE || CASE WHEN c.DATA_LENGTH IS NOT NULL THEN '(' || c.DATA_LENGTH || ')' END AS column_type,
c.DATA_TYPE AS data_type,
c.DATA_LENGTH AS char_max_length,
c.DATA_PRECISION AS numeric_precision,
c.DATA_SCALE AS numeric_scale,
c.NULLABLE AS is_nullable,
c.DATA_DEFAULT AS column_default,
cc.COMMENTS AS column_comment,
NULL AS extra
FROM ALL_TAB_COLUMNS c
LEFT JOIN ALL_COL_COMMENTS cc
ON cc.OWNER = c.OWNER
AND cc.TABLE_NAME = c.TABLE_NAME
AND cc.COLUMN_NAME = c.COLUMN_NAME
WHERE c.OWNER = ?
ORDER BY c.TABLE_NAME, c.COLUMN_ID
-- ============================================================================
-- 列出指定 schema 下所有列的元数据 (MySQL 方言)
-- 调用方:web/core/db_adapter.py → list_columns()
-- 参数:schema 数据库/模式名(绑定参数,由调用方通过 params 传入,勿拼接)
-- 注意:本文件不得出现其它百分号字面量,否则会被驱动误认为占位符
-- 返回字段:table_name, column_name, ordinal_position, column_type,
-- data_type, char_max_length, numeric_precision, numeric_scale,
-- is_nullable, column_default, column_comment, extra
-- ============================================================================
SELECT
c.TABLE_NAME AS table_name,
c.COLUMN_NAME AS column_name,
c.ORDINAL_POSITION AS ordinal_position,
c.COLUMN_TYPE AS column_type,
c.DATA_TYPE AS data_type,
c.CHARACTER_MAXIMUM_LENGTH AS char_max_length,
c.NUMERIC_PRECISION AS numeric_precision,
c.NUMERIC_SCALE AS numeric_scale,
c.IS_NULLABLE AS is_nullable,
c.COLUMN_DEFAULT AS column_default,
c.COLUMN_COMMENT AS column_comment,
c.EXTRA AS extra
FROM INFORMATION_SCHEMA.COLUMNS c
WHERE c.TABLE_SCHEMA = %s
ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION
\ No newline at end of file
-- ============================================================================
-- 列出指定 schema 下所有列的元数据 (Oracle 方言)
-- 调用方:web/core/db_adapter.py → list_columns()
-- 参数:schema 用户/模式名(绑定参数,调用方通过 params 传入,勿拼接)
-- 说明:
-- - Oracle 没有 INFORMATION_SCHEMA,使用 ALL_TAB_COLUMNS(结构与达梦高度一致)
-- - Oracle 的 OWNER 通常为大写;调用方传入的小写 schema 也用 UPPER() 兼容
-- - 占位符用 :1(oracledb 接收 named/numbered binding)
-- - DATA_TYPE 在字符类型上拼 DATA_LENGTH;NUMBER 类型没有 DATA_LENGTH,单独显示
-- - is_nullable 列在 Oracle 中是 NULLABLE(取值 'Y'/'N'),与达梦相同
-- - 列注释不在 ALL_TAB_COLUMNS 里,要 LEFT JOIN ALL_COL_COMMENTS
-- ============================================================================
SELECT
c.TABLE_NAME AS table_name,
c.COLUMN_NAME AS column_name,
c.COLUMN_ID AS ordinal_position,
CASE
WHEN c.DATA_TYPE IN ('VARCHAR2','NVARCHAR2','CHAR','RAW')
THEN c.DATA_TYPE || '(' || c.DATA_LENGTH || ')'
ELSE c.DATA_TYPE
END AS column_type,
c.DATA_TYPE AS data_type,
c.DATA_LENGTH AS char_max_length,
c.DATA_PRECISION AS numeric_precision,
c.DATA_SCALE AS numeric_scale,
c.NULLABLE AS is_nullable,
c.DATA_DEFAULT AS column_default,
cc.COMMENTS AS column_comment,
NULL AS extra
FROM ALL_TAB_COLUMNS c
LEFT JOIN ALL_COL_COMMENTS cc
ON cc.OWNER = c.OWNER
AND cc.TABLE_NAME = c.TABLE_NAME
AND cc.COLUMN_NAME = c.COLUMN_NAME
WHERE c.OWNER = UPPER(:1)
ORDER BY c.TABLE_NAME, c.COLUMN_ID
-- ============================================================================
-- 列出指定 schema 下所有表 (达梦方言)
-- 调用方:web/core/db_adapter.py → list_tables()
-- 参数:schema 数据库/模式名(绑定参数,由调用方通过 params 传入,勿拼接)
-- 说明:
-- - 达梦 ALL_TABLES 没有 BYTES 列(Oracle 才有),DATA_LENGTH 给 NULL
-- - 达梦 ALL_TABLES 也没有 COMMENTS 列,表注释存在 ALL_TAB_COMMENTS 里,
-- 要 LEFT JOIN 出来(与 ALL_COL_COMMENTS / ALL_TAB_COLUMNS 同源结构)
-- - 达梦 ALL_TABLES 也没有 CREATED / LAST_DDL 列(时间戳一概没有),
-- 真要看得 LEFT JOIN ALL_OBJECTS(按 OBJECT_TYPE='TABLE' 过滤),
-- 字段治理主线不用时间戳,先给 NULL 兜底
-- ============================================================================
SELECT t.TABLE_NAME AS table_name,
'BASE TABLE' AS table_type,
NULL AS engine,
t.NUM_ROWS AS table_rows,
NULL AS data_length,
0 AS index_length,
tc.COMMENTS AS table_comment,
NULL AS create_time,
NULL AS update_time
FROM ALL_TABLES t
LEFT JOIN ALL_TAB_COMMENTS tc
ON tc.OWNER = t.OWNER
AND tc.TABLE_NAME = t.TABLE_NAME
WHERE t.OWNER = UPPER(?)
ORDER BY t.TABLE_NAME
\ No newline at end of file
-- ============================================================================
-- 列出指定 schema 下所有表 (MySQL 方言)
-- 调用方:web/core/db_adapter.py → list_tables()
-- 参数:schema 数据库/模式名(绑定参数,由调用方通过 params 传入,勿拼接)
-- 注意:本文件不得出现其它百分号字面量,否则会被驱动误认为占位符
-- ============================================================================
SELECT TABLE_NAME, TABLE_TYPE, ENGINE, TABLE_ROWS,
DATA_LENGTH, INDEX_LENGTH, TABLE_COMMENT,
CREATE_TIME, UPDATE_TIME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = %s
ORDER BY TABLE_NAME
\ No newline at end of file
-- ============================================================================
-- 列出指定 schema 下所有表 (Oracle 方言)
-- 调用方:web/core/db_adapter.py → list_tables()
-- 参数:schema 用户/模式名(绑定参数,调用方通过 params 传入,勿拼接)
-- 说明:
-- - Oracle 没有 INFORMATION_SCHEMA,使用 ALL_TABLES(结构与达梦高度一致)
-- - Oracle ALL_TABLES 没有 COMMENTS 列,表注释在 ALL_TAB_COMMENTS 里,
-- LEFT JOIN 出来(与达梦同源——同一类问题同一个修法)
-- - 2026-08-13:实测 11g (11.2.0.4) ALL_TABLES 实际可用列清单(用户跑 DESC ALL_TABLES 确认):
-- OWNER / TABLE_NAME / TABLESPACE_NAME / CLUSTER_NAME / IOT_NAME / STATUS /
-- PCT_FREE / PCT_USED / INI_TRANS / MAX_TRANS /
-- INITIAL_EXTENT / NEXT_EXTENT / MIN_EXTENTS / MAX_EXTENTS / PCT_INCREASE /
-- FREELISTS / FREELIST_GROUPS / LOGGING / BACKED_UP /
-- NUM_ROWS / BLOCKS / EMPTY_BLOCKS / AVG_SPACE / CHAIN_CNT / AVG_ROW_LEN /
-- AVG_SPACE_FREELIST_BLOCKS / NUM_FREELIST_BLOCKS
-- - 11g ALL_TABLES **没有** BYTES / CREATED / LAST_DDL_TIME 列(这三个都是 12c+ 加的),
-- 所以 data_length 用 BLOCKS * 8192 估算(Oracle 默认 block size 8KB;空表为 0,未分配 segment),
-- create_time / update_time 取 NULL。若以后接 12c+,新增 list_tables.oracle.12c.sql
-- 即可(sql_loader 自动优先选方言版本,不影响 11g)。
-- ============================================================================
SELECT t.TABLE_NAME AS table_name,
'BASE TABLE' AS table_type,
NULL AS engine,
t.NUM_ROWS AS table_rows,
t.BLOCKS * 8192 AS data_length, -- 11g 没 BYTES,用 BLOCKS×8K 估算
0 AS index_length,
tc.COMMENTS AS table_comment,
NULL AS create_time, -- 11g ALL_TABLES 没有 CREATED(12c+)
NULL AS update_time -- 11g ALL_TABLES 没有 LAST_DDL_TIME(12c+)
FROM ALL_TABLES t
LEFT JOIN ALL_TAB_COMMENTS tc
ON tc.OWNER = t.OWNER
AND tc.TABLE_NAME = t.TABLE_NAME
WHERE t.OWNER = UPPER(:1)
ORDER BY t.TABLE_NAME
fastapi>=0.110 fastapi>=0.110
uvicorn[standard]>=0.27 uvicorn[standard]>=0.27
sqlalchemy>=2.0 sqlalchemy>=2.0
pydantic>=2.5 pydantic>=2.5
\ No newline at end of file # 数据库驱动(web3/backend/core/db_adapter.py 需要;dmPython 装不上不影响启动,只影响连达梦)
pymysql>=1.1
oracledb>=1.4
dmPython>=2.5
\ No newline at end of file
"""web3 数据库连接 API
端点:
POST /api/connect/test 测试连接(MySQL / Oracle / 达梦)
POST /api/connect/schemas 列出可用的 schema / database / user
POST /api/connect/tables 列出指定 schema 下的所有表
实现:
- 复用 web3/backend/core/db_adapter(自包含,不依赖 web.core)
- db_type 兼容前端中文 label("MySQL"/"Oracle"/"达梦 DM"),内部统一小写
- 错误用 logger.exception 拿全栈,响应 message 给前端弹窗
- 错误分类提示(WinError 10061 / timeout / Access denied)写到 logs/app.log
"""
from __future__ import annotations
import time
from typing import Any
from fastapi import APIRouter
from web3.backend.core.db_adapter import DBConfig, DBConnection
from web3.backend.core.sql_loader import get_sql_loader
from web3.backend.core.models import TestConnectionRequest, TestConnectionResponse
from web3.backend._logging import get_logger
router = APIRouter(prefix="/connect", tags=["database"])
logger = get_logger("backend.routers.db")
# ── db_type 映射 ──────────────────────────────────────────
# 前端 form.dbType 是中文 label("MySQL"/"Oracle"/"达梦 DM"),内部统一小写(mysql/oracle/dameng)
_LABEL_TO_DB_TYPE = {
"MySQL": "mysql",
"Oracle": "oracle",
"达梦 DM": "dameng",
}
def _normalize_db_type(db_type: str) -> str:
"""把前端传入的 db_type 归一化成小写内部值"""
return _LABEL_TO_DB_TYPE.get(db_type, db_type.lower())
def _build_db_config(req: TestConnectionRequest) -> DBConfig:
"""把 TestConnectionRequest 转换成 DBConfig"""
return DBConfig(
db_type=_normalize_db_type(req.db_type),
host=req.host,
port=req.port,
user=req.user,
password=req.password,
database=req.database,
charset=req.charset,
connect_timeout=req.connect_timeout,
oracle_client_dir=req.oracle_client_dir,
)
# ── 工具函数 ─────────────────────────────────────────────
def _mask_password(cfg: DBConfig) -> str:
"""DBConfig 脱敏输出(用于日志)"""
return (
f"db_type={cfg.db_type} "
f"host={cfg.host}:{cfg.port} "
f"user={cfg.user} "
f"password=*** "
f"database={cfg.database}"
)
def _log_failure_hint(msg: str) -> None:
"""根据错误 message 分类写日志(给运维看,不影响响应)"""
if "WinError 10061" in msg or "Connection refused" in msg:
logger.warning(
" 💡 诊断: 目标端口未监听(连接被拒)。可能原因:\n"
" 1) 主机/端口写错;\n"
" 2) 数据库服务没启;\n"
" 3) 防火墙拦截;\n"
" 4) 数据库不在当前网络可达段"
)
elif "timed out" in msg.lower() or "timeout" in msg.lower():
logger.warning(
" 💡 诊断: 连接超时。可能原因:\n"
" 1) 主机不可达(IP 错 / 不在同一网段);\n"
" 2) 防火墙丢包;\n"
" 3) 数据库服务卡死但不拒绝连接"
)
elif "Access denied" in msg or "1045" in msg:
logger.warning(" 💡 诊断: 用户名或密码错")
elif "Unknown database" in msg or "1049" in msg:
logger.warning(" 💡 诊断: 数据库名不存在")
# ── 端点 ─────────────────────────────────────────────────
@router.post("/test", response_model=TestConnectionResponse, summary="测试数据库连接")
async def connect_test(req: TestConnectionRequest):
"""复用 web3.backend.core.db_adapter,验证 db_type/host/port/user/password/database
返回 TestConnectionResponse(ok, message, db_type, database_name)
"""
cfg = _build_db_config(req)
logger.info("─" * 60)
logger.info(f"POST /api/connect/test {_mask_password(cfg)}")
started = time.monotonic()
try:
with DBConnection(cfg) as db:
sql = get_sql_loader().render("health/check_connection", dialect=cfg.db_type)
v = db.fetch_scalar(sql)
elapsed_ms = (time.monotonic() - started) * 1000
if v == 1:
msg = f"连接成功({cfg.db_type} @ {cfg.host}:{cfg.port}/{cfg.database},耗时 {elapsed_ms:.0f}ms)"
logger.info(f"✅ {msg}")
logger.info("─" * 60)
return TestConnectionResponse(
ok=True,
message=msg,
db_type=cfg.db_type,
database_name=cfg.database,
)
msg = "连接成功但查询返回非预期值"
logger.warning(f"❌ {msg}(耗时 {elapsed_ms:.0f}ms)")
logger.info("─" * 60)
return TestConnectionResponse(ok=False, message=msg, db_type=cfg.db_type)
except Exception as e:
elapsed_ms = (time.monotonic() - started) * 1000
msg = f"连接失败: {type(e).__name__}: {e}"
# logger.exception 自动带全栈
logger.exception(f"❌ 测试连接抛出异常(耗时 {elapsed_ms:.0f}ms)")
_log_failure_hint(str(e))
logger.info("─" * 60)
return TestConnectionResponse(ok=False, message=msg, db_type=cfg.db_type)
@router.post("/schemas", summary="列出可用的 schema / database / user")
async def list_schemas(req: TestConnectionRequest):
"""根据 db_type 列出可连接的 schema 列表
- MySQL = SHOW DATABASES
- 达梦 = SELECT DISTINCT OWNER FROM ALL_TABLES(排除系统用户)
- Oracle = SELECT username FROM all_users(排除系统用户)
"""
cfg = _build_db_config(req)
logger.info("─" * 60)
logger.info(f"POST /api/connect/schemas {_mask_password(cfg)}")
try:
with DBConnection(cfg) as db:
rows: list[dict[str, Any]] = []
if cfg.db_type == "mysql":
rows = db.fetchall("SHOW DATABASES")
schemas = [r.get("Database", r.get("database", "")) for r in rows]
elif cfg.db_type == "dameng":
rows = db.fetchall(
"SELECT DISTINCT OWNER AS schema_name FROM ALL_TABLES "
"WHERE OWNER NOT IN ('SYS','SYSTEM','SYSAUDITOR','CTISYS') "
"ORDER BY OWNER"
)
schemas = [r["schema_name"] for r in rows]
elif cfg.db_type == "oracle":
rows = db.fetchall(
"SELECT username AS schema_name FROM all_users "
"WHERE username NOT IN ('SYS','SYSTEM','OUTLN','DIP',"
"'ORACLE_OCM','XDB','ANONYMOUS','CTXSYS','MDSYS','OLAPSYS',"
"'ORDDATA','APEX_030200','APEX_PUBLIC_USER','FLOWS_FILES') "
"ORDER BY username"
)
schemas = [r["schema_name"] for r in rows]
else:
msg = f"不支持的 db_type: {cfg.db_type}"
logger.warning(f"❌ {msg}")
logger.info("─" * 60)
return {"ok": False, "message": msg, "schemas": []}
logger.info(f"✅ 返回 {len(schemas)} 个 schema")
logger.info("─" * 60)
return {"ok": True, "schemas": schemas, "count": len(schemas)}
except Exception as e:
msg = f"获取 schema 列表失败: {type(e).__name__}: {e}"
logger.exception(f"❌ {msg}")
_log_failure_hint(str(e))
logger.info("─" * 60)
return {"ok": False, "message": msg, "schemas": []}
@router.post("/tables", summary="列出指定 schema 下的所有表")
async def list_tables(req: TestConnectionRequest):
"""复用 db_adapter.list_tables(),走 info_schema/list_tables.<dialect>.sql 模板"""
cfg = _build_db_config(req)
logger.info("─" * 60)
logger.info(f"POST /api/connect/tables {_mask_password(cfg)} schema={req.database!r}")
try:
with DBConnection(cfg) as db:
rows = db.list_tables(req.database)
logger.info(f"✅ 返回 {len(rows)} 张表")
logger.info("─" * 60)
return {"ok": True, "tables": rows, "count": len(rows)}
except Exception as e:
msg = f"获取表列表失败: {type(e).__name__}: {e}"
logger.exception(f"❌ {msg}")
_log_failure_hint(str(e))
logger.info("─" * 60)
return {"ok": False, "message": msg, "tables": []}
\ No newline at end of file
/**
* 数据库连接 API(2026-08-20 接入真实后端)
*
* 前端 form.dbType 是中文 label("MySQL"/"Oracle"/"达梦 DM"),
* 后端 /api/connect/* 端点统一接收小写(mysql/oracle/dameng),这里做归一化。
*/
import { http } from './client'
const DB_TYPE_MAP = {
'MySQL': 'mysql',
'Oracle': 'oracle',
'达梦 DM': 'dameng',
}
/**
* 把前端 form 转成后端 TestConnectionRequest 字段
* - dbType 中文 label → 内部小写
* - port 字符串 → number
* - database 用 form.db(前端字段名是 db,后端字段是 database)
*/
function buildPayload(form) {
return {
db_type: DB_TYPE_MAP[form.dbType] || form.dbType,
host: form.host,
port: Number(form.port) || 0,
user: form.user,
password: form.password,
database: form.db,
charset: 'utf8mb4',
connect_timeout: 10,
}
}
/**
* 测试数据库连接
* @returns { ok, message, db_type, database_name, tables_count, ... }
*/
export function testConnection(form) {
return http.post('/connect/test', buildPayload(form))
}
/**
* 列出可用的 schema / database / user
* @returns { ok, schemas: string[], count, message? }
*/
export function listSchemas(form) {
return http.post('/connect/schemas', buildPayload(form))
}
/**
* 列出指定 schema 下的所有表
* @returns { ok, tables: [{table_name, table_comment, ...}], count, message? }
*/
export function listTables(form) {
return http.post('/connect/tables', buildPayload(form))
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment