Commit 562894a9 authored by Data Governance Dev's avatar Data Governance Dev

feat(web2): /api/db-defaults 改读 web2 yaml + 列表结构返回

- 路径:web/configs/db_defaults.yaml → web2/configs/db_defaults.yaml
  (web2 与 web1 配置完全分离,2026-08-17 起)
- 新增 _normalize_defaults():
  - 新格式 {db_type: [conn, ...]} → 直接归一化(自动注入 id + db_type)
  - 旧格式 {db_type: {conn}} → 包成单元素 list(向后兼容)
- 返回结构:{mysql: [{...}, ...], dameng: [...], oracle: [...]}
  每个连接有 id + db_type(自动注入,避免前端漏字段 → FastAPI 422)
- _mask_password 加 id 字段打印,日志输出加「总连接数 + 各 type 数」统计
parent ec0d1323
"""数据库连接相关 API
端点:
GET /api/db-defaults 读取 web/configs/db_defaults.yaml,返回三套预设
GET /api/db-defaults 读取 web2/configs/db_defaults.yaml,返回多套连接列表
POST /api/connect/test 测试连接是否可用(复用 web.core.db_adapter)
实现说明:
- 直接读 web/configs/db_defaults.yaml,不在 web2/ 复制一份(避免漂移)
- web2 用独立的 configs/db_defaults.yaml(与 web1 完全分离,2026-08-17 起)
- 结构:db_defaults[db_type] 是 list[Connection],每个连接有唯一 id
- 测试连接复用 web.core.db_adapter.DBConfig + test_connection()
- 请求/响应模型复用 web.core.models(与 web 端一致)
......@@ -35,9 +36,9 @@ router = APIRouter(tags=["database"])
# ── 路径常量 ────────────────────────────────────────────────
# web2/backend/routes/db.py → 往上 3 级到项目根 → web/configs/db_defaults.yaml
# web2/backend/routes/db.py → 往上 3 级到项目根 → web2/configs/db_defaults.yaml
PROJECT_ROOT = Path(__file__).resolve().parents[3]
WEB_DEFAULTS_YAML = PROJECT_ROOT / "web" / "configs" / "db_defaults.yaml"
WEB2_DEFAULTS_YAML = PROJECT_ROOT / "web2" / "configs" / "db_defaults.yaml"
# ── 工具函数 ────────────────────────────────────────────────
......@@ -46,6 +47,7 @@ def _mask_password(conn: dict) -> str:
safe = {k: ("***" if k == "password" else v) for k, v in conn.items()}
return (
f"db_type={safe.get('db_type')} "
f"id={safe.get('id', '<none>')} "
f"host={safe.get('host')}:{safe.get('port')} "
f"user={safe.get('user')} "
f"password={safe.get('password')} "
......@@ -54,42 +56,80 @@ def _mask_password(conn: dict) -> str:
)
def _normalize_defaults(raw_defaults: dict) -> dict:
"""把 yaml 解析后的 db_defaults 规范化为 web2 列表结构。
输入:
- 新格式(推荐):{ mysql: [{id, host, ...}, ...], dameng: [...], oracle: [...] }
- 旧格式(兼容):{ mysql: {host, port, ...}, ... }(每 db_type 是单个 dict)
输出:
{ mysql: [{id: '<db_type>', host, port, ... db_type: 'mysql', ...}, ...], ... }
旧格式会包成单元素 list,id 派生为 db_type。
"""
if not isinstance(raw_defaults, dict):
return {}
normalized: dict = {}
for db_type, value in raw_defaults.items():
# 旧格式:value 是 dict → 包成单元素 list
if isinstance(value, dict):
conn = {**value, "id": value.get("id") or db_type, "db_type": db_type}
normalized[db_type] = [conn]
continue
# 新格式:value 是 list
if isinstance(value, list):
conns = []
for idx, raw_conn in enumerate(value):
if not isinstance(raw_conn, dict):
logger.warning(
f"db_defaults[{db_type}][{idx}] 不是 dict,已跳过"
)
continue
# 缺 id 时派生一个(保证前端能用作 key)
conn_id = raw_conn.get("id") or f"{db_type}-{idx}"
conns.append({**raw_conn, "id": conn_id, "db_type": db_type})
normalized[db_type] = conns
continue
logger.warning(f"db_defaults[{db_type}] 类型异常({type(value).__name__}),已跳过")
return normalized
# ── 端点 ────────────────────────────────────────────────────
@router.get("/db-defaults", summary="读取数据库连接预设")
async def db_defaults():
"""读取 web/configs/db_defaults.yaml,返回三套预设(mysql/dameng/oracle)
"""读取 web2/configs/db_defaults.yaml,返回多套连接列表
结构(2026-08-17 起):
{
"mysql": [ {id: "smart-build", host, port, ...}, {id: "backup", ...} ],
"dameng": [ {id: "sanjin", host, port, ...} ],
"oracle": [ {id: "huanggang-gjj", host, port, ...} ],
}
文件不存在或解析失败时返回空 dict,前端保留 JS 内置兜底值。
每个连接必须有 id(用于前端 localStorage 持久化 + 列表 key)。
文件缺失 / 解析失败时返回空 dict,前端保留 JS 内置兜底值。
"""
logger.info("─" * 60)
logger.info("GET /api/db-defaults")
if not WEB_DEFAULTS_YAML.exists():
logger.warning(f"db_defaults.yaml 缺失: {WEB_DEFAULTS_YAML}")
if not WEB2_DEFAULTS_YAML.exists():
logger.warning(f"db_defaults.yaml 缺失: {WEB2_DEFAULTS_YAML}")
logger.info("─" * 60)
return {}
try:
raw = yaml.safe_load(WEB_DEFAULTS_YAML.read_text(encoding="utf-8")) or {}
raw = yaml.safe_load(WEB2_DEFAULTS_YAML.read_text(encoding="utf-8")) or {}
except Exception as e:
logger.exception(f"db_defaults.yaml 解析失败: {e}")
logger.info("─" * 60)
return {}
defaults = raw.get("db_defaults", {})
# 旧结构兼容:如果顶层 db_defaults 的 value 不是 dict,就当成「老格式单套」包成 {mysql: ...}
if not isinstance(defaults, dict) or any(
not isinstance(v, dict) for v in defaults.values()
):
logger.warning("db_defaults.yaml 为旧格式(单套配置),仅 mysql 可用")
logger.info(f"返回 keys: ['mysql'](旧格式兼容)")
logger.info("─" * 60)
return {"mysql": defaults}
# 防御性注入 db_type:yaml 里每个预设只有 host/port/user/password/database,
# 没有 db_type 字段(db_type 是 key 名)。前端有时会拿 conn.db_type 用,
# 后端注入后两边都安全(避免前端 JSON.stringify 漏掉 db_type → FastAPI 422)。
for db_type, preset in defaults.items():
if isinstance(preset, dict):
preset.setdefault("db_type", db_type)
raw_defaults = raw.get("db_defaults", {})
defaults = _normalize_defaults(raw_defaults)
total_conns = sum(len(v) for v in defaults.values() if isinstance(v, list))
logger.info(
f"返回 keys: {sorted(defaults.keys())}({sum(len(v) for v in defaults.values() if isinstance(v, dict))} 个字段)"
f"返回 keys: {sorted(defaults.keys())}({total_conns} 个连接,"
f"{ {k: len(v) for k, v in defaults.items()} })"
)
logger.info("─" * 60)
return defaults
......
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