Commit 9646cd1a authored by Data Governance Dev's avatar Data Governance Dev

feat(web): 三数据库适配(MySQL/达梦/Oracle)+ 连接默认值独立配置

【Oracle 适配 — 端到端接入】
- web/core/db_adapter.py
  · 新增 to_oracle_kwargs(),oracledb 1.4.x 关键字映射
  · _select_driver() 新增 oracle 分支,未安装 oracledb 时给中文友好报错
  · _ensure_oracle_client() 自动检测 Instant Client,老 Oracle 切 thick 模式
  · DBConfig.oracle_client_dir 字段(显式 lib_dir)
  · TEXT_TYPES 补 Oracle 类型(nclob / long)
- web/sql/info_schema/list_columns.oracle.sql + list_tables.oracle.sql
  · 新增 Oracle 方言的字典查询模板(ALL_TAB_COLUMNS / ALL_TABLES)
  · 表注释 LEFT JOIN ALL_TAB_COMMENTS(同达梦结构)
- web/sql/standards/{sample_field_groups,sample_field_groups_single,
  sample_field_pairs,sample_field_pairs_two_text}.sql
  · \`\${col}\` → \${col | quote} 跨方言重构,加新数据库零 SQL 改动
- web/sql/loader.py:dialect 白名单加 "oracle"
- web/requirements.txt:锁定 oracledb>=1.4.2,<2.0(兼容 Oracle 11.2)
- web/start.py:start 脚本新增 oracledb / dmPython 可选组件检测
- web/app.py / web/core/__init__.py / web/core/step_impl/__init__.py /
  web/core/models.py / web/configs/defaults.yaml / web/README.md
  · 描述/注释/枚举更新为「MySQL / 达梦 / Oracle」
  · models.ConnectRequest 加 oracle_client_dir 字段
  · README 加 Oracle 行 + 三端口映射说明

【达梦适配 — 跑通实测】
- web/core/db_adapter.py
  · to_dm_kwargs() 改用 server= 关键字(不是 host=),删 dmPython 不支持的
    schema / charset / connect_timeout 参数(C 扩展传错会 SystemError 吞真实异常)
  · _rows_as_dicts() 统一 PyMySQL Dict / tuple 两种行格式
  · fetchall / fetchone 从 cur.description 取列名兜底
  · dmPython SystemError 增强日志(提示真实原因 + 给排查脚本路径)
- web/sql/info_schema/list_columns.dameng.sql
  · 列注释 LEFT JOIN ALL_COL_COMMENTS(修 [CODE:-2207] C.COMMENTS)
- web/sql/info_schema/list_tables.dameng.sql
  · BYTES → NULL(达梦 ALL_TABLES 无此列)
  · COMMENTS → LEFT JOIN ALL_TAB_COMMENTS(达梦 ALL_TABLES 无此列)
  · CREATED / LAST_DDL_TIME → NULL(达梦 ALL_TABLES 无时间戳)
    真要看时间戳得 LEFT JOIN ALL_OBJECTS,按 OBJECT_TYPE='TABLE' 过滤

【UI 改进】
- web/static/index.html
  · 数据库类型三选项(MySQL / 达梦 / Oracle)
  · onDbTypeChange 端口切换改用 DB_DEFAULT_PORTS 映射表
    (mysql:3306 / dameng:5236 / oracle:1521),加守卫保留用户自定义端口
  · loadDbDefaults() 启动时从 /api/db-defaults 读连接默认值填表单
  · charset 下拉新增 al32utf8 / zhs16gbk(Oracle NLS 字符集)
  · Oracle radio 暂未实装完成,先注释隐藏(后端代码保留,放开零成本)

【连接默认值独立配置】
- web/configs/db_defaults.yaml(新增,.gitignore 排除,存真实凭据)
- web/api/routes.py:新增 GET /api/db-defaults 端点
- .gitignore:补 web/configs/db_defaults.yaml 防止真实凭据误提交

【诊断与日志】
- scripts/_probe_dm.py:达梦连接独立诊断工具(C 扩展吞异常时排查用)
- docs/WORKLOG.md:本次任务完整工作记录(含三库字典表差异表)

【未提交】
- web/configs/db_defaults.yaml(真实密码,gitignore 排除)
- scripts/_tmp_build_violations_md.py(一次性调试脚本,硬编码绝对路径)
- data_dictionary/standard_fields_violations_report.md(生成报告,非源码)
parent 0f08eab6
...@@ -13,6 +13,7 @@ web/outputs/ ...@@ -13,6 +13,7 @@ web/outputs/
# ── 敏感配置(API Key / 真实凭据) ── # ── 敏感配置(API Key / 真实凭据) ──
web/configs/llm.yaml web/configs/llm.yaml
web/configs/db_defaults.yaml
workflow/config.yaml workflow/config.yaml
# ── Python 字节码缓存 ── # ── Python 字节码缓存 ──
......
This diff is collapsed.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""达梦连接独立诊断脚本
用法:
python scripts/_probe_dm.py
# 或带参:
python scripts/_probe_dm.py --host 192.168.20.24 --port 5236 \
--user HGWXZJ --password *** --schema HGWXZJ
为什么需要它:
dmPython 是 C 扩展,连接失败时经常抛 SystemError 而把真实异常吃掉。
直接跑这个脚本,能看到 dmPython 内部到底报了什么错。
"""
from __future__ import annotations
import argparse
import socket
import sys
import traceback
from pathlib import Path
def tcp_probe(host: str, port: int, timeout: float = 5.0) -> None:
"""先做个 TCP 探测,确认网络通 + 端口开着"""
print(f"\n[1/4] TCP 探测 {host}:{port} ...")
try:
with socket.create_connection((host, port), timeout=timeout) as s:
print(f" ✓ TCP 通,端口开着")
# 试着读一下 banner(达梦服务端会主动发 banner)
try:
s.settimeout(2.0)
banner = s.recv(256)
print(f" banner: {banner!r}")
except socket.timeout:
print(f" (无 banner 或超时——这不一定代表错)")
except (socket.timeout, ConnectionRefusedError, OSError) as e:
print(f" ✗ TCP 不通: {type(e).__name__}: {e}")
print(f" → 网络/防火墙/达梦服务问题,先确认 dm 服务在跑、端口对得上")
def probe_dm_python(host: str, port: int, user: str, password: str,
schema: str, charset: str, connect_timeout: int) -> None:
"""直接调 dmPython.connect 看真实异常"""
print(f"\n[2/4] dmPython 版本与可导入性 ...")
try:
import dmPython
print(f" ✓ dmPython 已装")
print(f" 包位置: {getattr(dmPython, '__file__', '未知')}")
print(f" __version__: {getattr(dmPython, '__version__', '未知(老版本或二进制 wheel 不暴露)')}")
except ImportError:
print(f" ✗ dmPython 未装,请先 pip install dmPython")
return
# ---- pip show dmPython(拿到版本号 + 安装来源) ----
print(f"\n pip show dmPython:")
try:
import subprocess
r = subprocess.run(
[sys.executable, "-m", "pip", "show", "dmPython"],
capture_output=True, text=True, timeout=15,
)
if r.returncode == 0:
for line in r.stdout.splitlines():
if line.startswith(("Version:", "Location:", "Source:", "Requires:")):
print(f" {line}")
else:
print(f" (pip show 失败: {r.stderr.strip().splitlines()[0] if r.stderr else '?'})")
except Exception as e:
print(f" (无法执行 pip show: {e})")
# ---- 查找本机达梦客户端动态库 ----
print(f"\n 本机达梦客户端动态库扫描:")
found = []
for pat in ("dmdpi.dll", "libdmdpi.so", "libdmdpi.so.1"):
# PATH 目录 + 常见安装路径
candidates = []
# 从 PATH
for p in sys.path:
if "site-packages" in p or "dist-packages" in p:
continue
candidates.append(Path(p) / pat)
# 常见路径
candidates += [
Path(r"C:\dmdbms"),
Path(r"C:\Program Files\dmdbms"),
Path(r"D:\dmdbms"),
Path("/opt/dmdbms"),
Path("/usr/lib"),
Path("/usr/local/lib"),
]
for c in candidates:
if c.exists():
found.append(str(c))
if found:
for f in sorted(set(found)):
print(f" ✓ {f}")
else:
print(f" ✗ 未找到 dmdpi.dll / libdmdpi.so —— 达梦客户端可能没装")
print(f"\n[3/4] dmPython.connect({host}:{port}, user={user}, schema={schema}, charset={charset}) ...")
try:
conn = dmPython.connect(
host=host, port=port,
user=user, password=password,
schema=schema, charset=charset,
connect_timeout=connect_timeout,
)
print(f" ✓ 连接成功!")
cur = conn.cursor()
cur.execute("SELECT 1")
print(f" ✓ SELECT 1 返回: {cur.fetchone()}")
cur.close()
conn.close()
return
except SystemError as e:
# 达梦真实错误被 C 层吃了 —— 只能给诊断建议
print(f" ✗ SystemError(C 扩展吞了真实异常): {e}")
print(f" 这是 dmPython 的经典坑。真实错误通常是:")
print(f" a) dmPython 包版本与本机达梦客户端(libdmdpi.so / dmdpi.dll)版本不匹配")
print(f" → 重装匹配版本的 dmPython,或升级/降级达梦客户端")
print(f" b) charset 不被达梦支持")
print(f" → 试试 charset='GB18030' 或 'UTF-8'(不要 utf8mb4)")
print(f" c) 服务名 / 账号 / 密码错(达梦错误码被吃了)")
print(f" → 用 disql 连同一组参数验证一下")
except Exception as e:
print(f" ✗ {type(e).__name__}: {e}")
traceback.print_exc()
print(f"\n[4/4] 改 charset=GB18030 再试一次 ...")
try:
conn = dmPython.connect(
host=host, port=port,
user=user, password=password,
schema=schema, charset="GB18030",
connect_timeout=connect_timeout,
)
print(f" ✓ GB18030 连接成功!→ charset 是不支持的 utf8mb4,改成 GB18030")
conn.close()
return
except Exception as e:
print(f" ✗ GB18030 也失败: {type(e).__name__}: {e}")
if isinstance(e, SystemError):
print(f" → charset 不是原因。剩余最可能:")
print(f" - dmPython 包与本机达梦客户端版本不匹配")
print(f" - 本机没装达梦客户端(只有 pip 的 dmPython wheel)")
print(f" - 账号/密码/服务名错(被 C 层吃了)")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="192.168.20.24")
parser.add_argument("--port", type=int, default=5236)
parser.add_argument("--user", default="HGWXZJ")
parser.add_argument("--password", default="", help="(不回显,建议直接跑交互式填)")
parser.add_argument("--schema", default="HGWXZJ")
parser.add_argument("--charset", default="utf8mb4")
parser.add_argument("--connect-timeout", type=int, default=10)
args = parser.parse_args()
if not args.password:
import getpass
args.password = getpass.getpass("达梦密码: ")
print("=" * 60)
print(f"达梦连接诊断: {args.host}:{args.port} schema={args.schema}")
print("=" * 60)
tcp_probe(args.host, args.port)
probe_dm_python(
args.host, args.port, args.user, args.password,
args.schema, args.charset, args.connect_timeout,
)
print("\n排查建议优先级:")
print(" 1. 看 TCP 探测是否通")
print(" 2. 看 charset 是不是 GB18030")
print(" 3. 用 disql 客户端同参数连一下,验证账号密码对不对")
print(" 4. 看 dmPython 与本机达梦客户端版本是不是匹配")
if __name__ == "__main__":
main()
...@@ -15,7 +15,7 @@ pip install -r web/requirements.txt ...@@ -15,7 +15,7 @@ pip install -r web/requirements.txt
依赖包括: 依赖包括:
- `fastapi` / `uvicorn` — Web 服务 - `fastapi` / `uvicorn` — Web 服务
- `pydantic` / `sse-starlette` — 数据模型 + SSE 流 - `pydantic` / `sse-starlette` — 数据模型 + SSE 流
- `pymysql` / `dmPython` — MySQL / 达梦驱动 - `pymysql` / `dmPython` / `oracledb` — MySQL / 达梦 / Oracle 驱动
- `anthropic` — Claude API(LLM 增强) - `anthropic` — Claude API(LLM 增强)
- `python-docx` — Word 报告生成 - `python-docx` — Word 报告生成
...@@ -111,7 +111,7 @@ python -m uvicorn web.app:app --host 0.0.0.0 --port 8765 ...@@ -111,7 +111,7 @@ python -m uvicorn web.app:app --host 0.0.0.0 --port 8765
| 特性 | 说明 | | 特性 | 说明 |
|------|------| |------|------|
| 🖥️ **Web 界面** | 浏览器访问,Vue 3 + Element Plus 响应式布局 | | 🖥️ **Web 界面** | 浏览器访问,Vue 3 + Element Plus 响应式布局 |
| 🔌 **双数据库** | MySQL(PyMySQL)+ 达梦(dmPython)统一接口 | | 🔌 **三数据库** | MySQL(PyMySQL)+ 达梦(dmPython)+ Oracle(oracledb)统一接口 |
| 🤖 **LLM 增强** | Claude API 用于字段注释推测 / 合并建议 / 长度推断 / 空字段分类 | | 🤖 **LLM 增强** | Claude API 用于字段注释推测 / 合并建议 / 长度推断 / 空字段分类 |
| 📊 **多级表格** | 6 大章节各一张可展开表格,KPI 卡片概览 | | 📊 **多级表格** | 6 大章节各一张可展开表格,KPI 卡片概览 |
| 📜 **国标展示** | 4 项内置标准可视化展示在页面(无需查询) | | 📜 **国标展示** | 4 项内置标准可视化展示在页面(无需查询) |
...@@ -139,7 +139,7 @@ python -m uvicorn web.app:app --host 0.0.0.0 --port 8765 ...@@ -139,7 +139,7 @@ python -m uvicorn web.app:app --host 0.0.0.0 --port 8765
│ │ ├── info_schema/ ← INFORMATION_SCHEMA 查询 │ │ ├── info_schema/ ← INFORMATION_SCHEMA 查询
│ │ ├── verify/ xzqh/ standards/ empty_fields/ health/ │ │ ├── verify/ xzqh/ standards/ empty_fields/ health/
│ ├── core/ ← 核心模块 │ ├── core/ ← 核心模块
│ │ ├── db_adapter.py ← MySQL + 达梦 统一接口 │ │ ├── db_adapter.py ← MySQL + 达梦 + Oracle 统一接口
│ │ ├── data_dict.py ← 数据字典抽取(connect-test 与 orchestrator 共用) │ │ ├── data_dict.py ← 数据字典抽取(connect-test 与 orchestrator 共用)
│ │ ├── data_dict_cache.py ← 数据字典进程内缓存(按连接身份) │ │ ├── data_dict_cache.py ← 数据字典进程内缓存(按连接身份)
│ │ ├── llm.py ← Claude API 封装 │ │ ├── llm.py ← Claude API 封装
...@@ -261,26 +261,38 @@ LLM 调用封装在 [web/core/llm.py](web/core/llm.py)。配置 Key 见上方「 ...@@ -261,26 +261,38 @@ LLM 调用封装在 [web/core/llm.py](web/core/llm.py)。配置 Key 见上方「
--- ---
## 🛡️ 双数据库适配说明 ## 🛡️ 三数据库适配说明
通过 [web/core/db_adapter.py](web/core/db_adapter.py) 统一抽象: 通过 [web/core/db_adapter.py](web/core/db_adapter.py) 统一抽象:
| 项 | MySQL | 达梦 | | 项 | MySQL | 达梦 | Oracle |
|----|-------|------| |----|-------|------|--------|
| 驱动 | `pymysql` | `dmPython` | | 驱动 | `pymysql` | `dmPython` | `oracledb` 1.4.x (thin) |
| 占位符 | `%s` | `:name` (NamedParam) | | 占位符 | `%s` | `?` | `:1` |
| 标识符引用 | `` ` `` (反引号) | `"` (双引号 ANSI) | | 标识符引用 | `` ` `` (反引号) | `"` (双引号 ANSI) | `"` (双引号 ANSI) |
| 字段大小写 | 视配置 | 默认大写 → 统一规范化为小写 | | 字段大小写 | 视配置 | 默认大写 → 统一规范化为小写 | 默认大写 → 统一规范化为小写 |
| 长文本类型 | `text/longtext/...` | `text/clob/longvarchar` | | 长文本类型 | `text/longtext/...` | `text/clob/longvarchar` | `varchar2/clob/nclob/long` |
| `INFORMATION_SCHEMA` | 完全支持 | 完全支持(字段名差异已处理) | | 数据字典 | `INFORMATION_SCHEMA.COLUMNS` | `ALL_TAB_COLUMNS` | `ALL_TAB_COLUMNS` |
| 默认端口 | 3306 | 5236 | 1521 |
> ⚠️ 达梦驱动 `dmPython` 需要本地先安装达梦客户端,否则 `pip install dmPython` 会失败。 > ⚠️ 达梦驱动 `dmPython` 需要本地先安装达梦客户端,否则 `pip install dmPython` 会失败。
> Oracle 用 `oracledb` thin 模式,**无需**安装 Oracle Instant Client;`database` 字段填 Oracle 的 service_name(如 `ORCLPDB1`),不是表 schema 名。
>
> ⚠️ **Oracle 版本兼容**:本仓库把 oracledb 锁在 **1.4.x**(`requirements.txt`),因为 2.0+ 的 thin 模式收紧了对老 Oracle 的支持,**Oracle 11.2 也会报 DPY-3010**:
>
> ```
> DPY-3010: connections to this database server version are not supported by python-oracledb in thin mode
> ```
>
> 1.4.x 的 thin 模式对 11.2 ~ 最新 Oracle 都正常。如果将来只连 12c+ / 19c / 21c,可以放开到 `oracledb>=2.0`。
>
> 若连接 **10g / 9i 等更老版本**,1.x 也报 DPY-3010,则需要装 [Oracle Instant Client](https://www.oracle.com/database/technologies/instant-client/downloads.html),应用会在首次连接时自动 `oracledb.init_oracle_client()` 切到 **thick 模式**(OCI 原生,支持 Oracle 9i+)。Instant Client 默认从 PATH / 注册表 / `ORACLE_HOME` 自动搜索;若不在默认路径,可在请求里显式传 `oracle_client_dir` 字段(如 `r"C:\oracle\instantclient_19_8"`)。
--- ---
## ⚠️ 已知事项 ## ⚠️ 已知事项
1. **MySQL 兼容**:所有 `INFORMATION_SCHEMA` 查询已用 ANSI 写法 + 适配层规范化,达梦可直接复用 1. **多数据库兼容**:所有 SQL 查询已用 ANSI 写法 + 适配层规范化,达梦 / Oracle 可直接复用;Oracle 数据字典走 `ALL_TAB_COLUMNS` / `ALL_TABLES`(无 `INFORMATION_SCHEMA`);Oracle thin 模式不支持的版本会自动尝试加载 Instant Client 切 thick 模式
2. **报告 bug 修复**:新版 Web 工具修正了原 `reporter.py` 的多处字段名不匹配(如 `over_provision_ratio`、`risk/suggestion` 等) 2. **报告 bug 修复**:新版 Web 工具修正了原 `reporter.py` 的多处字段名不匹配(如 `over_provision_ratio`、`risk/suggestion` 等)
3. **单进程**:JobManager 使用进程内 asyncio 队列,并发任务排队执行;如需多任务并行可改为 Celery 3. **单进程**:JobManager 使用进程内 asyncio 队列,并发任务排队执行;如需多任务并行可改为 Celery
4. **数据安全**:数据库密码仅在内存中传递,不写日志;提交前可勾选「启用 LLM」控制是否走外网 4. **数据安全**:数据库密码仅在内存中传递,不写日志;提交前可勾选「启用 LLM」控制是否走外网
...@@ -296,7 +308,7 @@ LLM 调用封装在 [web/core/llm.py](web/core/llm.py)。配置 Key 见上方「 ...@@ -296,7 +308,7 @@ LLM 调用封装在 [web/core/llm.py](web/core/llm.py)。配置 Key 见上方「
| 语法 | 含义 | 示例 | | 语法 | 含义 | 示例 |
|------|------|------| |------|------|------|
| `${var}` | 简单字符串替换 | `${schema}` | | `${var}` | 简单字符串替换 | `${schema}` |
| `${var \| quote}` | 按当前 dialect 自动加引号 | `${table \| quote}` → MySQL 反引号 / 达梦双引号 | | `${var \| quote}` | 按当前 dialect 自动加引号 | `${table \| quote}` → MySQL 反引号 / 达梦双引号 / Oracle 双引号 |
| `${list \| join:","}` | 列表按分隔符拼接 | `${cols \| join:","}` | | `${list \| join:","}` | 列表按分隔符拼接 | `${cols \| join:","}` |
### Dialect 加载规则 ### Dialect 加载规则
...@@ -314,6 +326,9 @@ sql = loader.render("info_schema/list_columns", dialect="mysql", schema="mydb") ...@@ -314,6 +326,9 @@ sql = loader.render("info_schema/list_columns", dialect="mysql", schema="mydb")
sql = loader.render("info_schema/list_columns", dialect="dameng", schema="mydb") sql = loader.render("info_schema/list_columns", dialect="dameng", schema="mydb")
# SELECT ... FROM ALL_TAB_COLUMNS c WHERE c.OWNER = "mydb" # SELECT ... FROM ALL_TAB_COLUMNS c WHERE c.OWNER = "mydb"
sql = loader.render("info_schema/list_columns", dialect="oracle", schema="mydb")
# SELECT ... FROM ALL_TAB_COLUMNS c WHERE c.OWNER = UPPER(:1)
``` ```
详细文档:[sql/README.md](sql/README.md) 详细文档:[sql/README.md](sql/README.md)
...@@ -323,7 +338,7 @@ sql = loader.render("info_schema/list_columns", dialect="dameng", schema="mydb") ...@@ -323,7 +338,7 @@ sql = loader.render("info_schema/list_columns", dialect="dameng", schema="mydb")
## 📝 后续可扩展 ## 📝 后续可扩展
- [ ] 添加更多国标插件(邮箱、银行卡、邮编等) - [ ] 添加更多国标插件(邮箱、银行卡、邮编等)
- [ ] 接入 jaydebeapi 作为达梦 JDBC 替代驱动 - [ ] 接入 jaydebeapi 作为达梦 JDBC 替代驱动(dmPython 装不上的兜底方案)
- [ ] 报告编辑器(在线编辑 Markdown / 一键导出) - [ ] 报告编辑器(在线编辑 Markdown / 一键导出)
- [ ] 多任务并行(Celery + Redis) - [ ] 多任务并行(Celery + Redis)
- [ ] 用户系统(FastAPI Users + JWT) - [ ] 用户系统(FastAPI Users + JWT)
......
...@@ -47,6 +47,30 @@ async def health(): ...@@ -47,6 +47,30 @@ async def health():
return {"status": "ok", "service": "data-governance-web"} return {"status": "ok", "service": "data-governance-web"}
# ── 数据库连接默认值 ──
# 从 web/configs/db_defaults.yaml 读取,供前端表单初始渲染使用
# 文件不存在或解析失败时返回空 dict,前端保留 JS 内置兜底值
@router.get("/db-defaults")
async def db_defaults():
import yaml # 延迟导入,yaml 是可选依赖的隐性兜底
cfg_path = Path(__file__).resolve().parents[2] / "web" / "configs" / "db_defaults.yaml"
if not cfg_path.exists():
logger.warning(f"db_defaults.yaml 缺失: {cfg_path}")
return {}
try:
raw = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
except Exception as e:
logger.warning(f"db_defaults.yaml 解析失败: {e}")
return {}
defaults = raw.get("db_defaults", {})
# 仅返回前端表单关心的字段,避免泄露无关配置
return {
k: defaults[k]
for k in ("host", "user", "password", "database")
if k in defaults
}
# ── LLM 状态(前端用来显示当前 provider) ── # ── LLM 状态(前端用来显示当前 provider) ──
@router.get("/llm/status") @router.get("/llm/status")
async def llm_status(): async def llm_status():
...@@ -191,6 +215,7 @@ async def connect_test(req: TestConnectionRequest): ...@@ -191,6 +215,7 @@ async def connect_test(req: TestConnectionRequest):
db_type=req.db_type, host=req.host, port=req.port, db_type=req.db_type, host=req.host, port=req.port,
user=req.user, password=req.password, database=req.database, user=req.user, password=req.password, database=req.database,
charset=req.charset, connect_timeout=req.connect_timeout, charset=req.charset, connect_timeout=req.connect_timeout,
oracle_client_dir=req.oracle_client_dir,
) )
ok, msg = test_connection(cfg) ok, msg = test_connection(cfg)
if not ok: if not ok:
......
...@@ -52,7 +52,7 @@ logger.info(f"日志目录: {LOG_DIR}") ...@@ -52,7 +52,7 @@ logger.info(f"日志目录: {LOG_DIR}")
# ── 创建 FastAPI 应用 ── # ── 创建 FastAPI 应用 ──
app = FastAPI( app = FastAPI(
title="数据治理 Web 工具", title="数据治理 Web 工具",
description="基于 FastAPI + Vue 3 的数据库治理平台(支持 MySQL / 达梦)", description="基于 FastAPI + Vue 3 的数据库治理平台(支持 MySQL / 达梦 / Oracle)",
version="1.0.0", version="1.0.0",
) )
logger.info("FastAPI 应用已创建") logger.info("FastAPI 应用已创建")
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
db_types: db_types:
- mysql - mysql
- dameng - dameng
- oracle
# 采样与阈值(与 CLI 工作流一致) # 采样与阈值(与 CLI 工作流一致)
thresholds: thresholds:
......
"""web.core 核心模块 """web.core 核心模块
- db_adapter: MySQL / 达梦 适配 - db_adapter: MySQL / 达梦 / Oracle 适配
- llm: Claude / OpenAI 客户端 - llm: Claude / OpenAI 客户端
- job_manager: 后台任务 + SSE - job_manager: 后台任务 + SSE
- orchestrator: 调度 8 步流程 - orchestrator: 调度 8 步流程
......
This diff is collapsed.
...@@ -11,7 +11,7 @@ from pydantic import BaseModel, Field ...@@ -11,7 +11,7 @@ from pydantic import BaseModel, Field
# ── 请求 ──────────────────────────────────────────────── # ── 请求 ────────────────────────────────────────────────
class ConnectRequest(BaseModel): class ConnectRequest(BaseModel):
"""前端「新建治理任务」表单提交""" """前端「新建治理任务」表单提交"""
db_type: str = Field(..., description="mysql / dameng") db_type: str = Field(..., description="mysql / dameng / oracle")
host: str host: str
port: int = 3306 port: int = 3306
user: str user: str
...@@ -19,6 +19,9 @@ class ConnectRequest(BaseModel): ...@@ -19,6 +19,9 @@ class ConnectRequest(BaseModel):
database: str database: str
charset: str = "utf8mb4" charset: str = "utf8mb4"
connect_timeout: int = 10 connect_timeout: int = 10
# Oracle 专用:Oracle Instant Client 的 lib_dir(oci.dll 所在目录)
# None = 自动搜索 PATH / 注册表;thin 模式报 DPY-3010 时显式指定
oracle_client_dir: Optional[str] = None
# 可选:要分析的步骤(按 step_id;为空表示跑全部) # 可选:要分析的步骤(按 step_id;为空表示跑全部)
steps: Optional[list[str]] = None steps: Optional[list[str]] = None
......
"""Step 实现包 — Web 版(兼容 MySQL + 达梦,接 LLM) """Step 实现包 — Web 版(兼容 MySQL + 达梦 + Oracle,接 LLM)
每个 step_* 函数接收通用 cfg + 上游 step_outputs,返回结构化 dict。 每个 step_* 函数接收通用 cfg + 上游 step_outputs,返回结构化 dict。
所有 Step 通过 web.core.orchestrator.run_governance_workflow() 调度。 所有 Step 通过 web.core.orchestrator.run_governance_workflow() 调度。
......
...@@ -10,6 +10,9 @@ pydantic>=2.5.0 ...@@ -10,6 +10,9 @@ pydantic>=2.5.0
# 数据库 # 数据库
pymysql>=1.1.0 # MySQL 驱动 pymysql>=1.1.0 # MySQL 驱动
dmPython>=2.5.0 # 达梦数据库驱动(需本地有达梦客户端) dmPython>=2.5.0 # 达梦数据库驱动(需本地有达梦客户端)
oracledb>=1.4.2,<2.0 # Oracle 数据库驱动 —— 锁定 1.x 是因为 2.0+ 的 thin 模式
# 收紧了 Oracle 版本支持,老版本(如 11.2)会报 DPY-3010
# 若以后只连 12c+/19c/21c,可放开到 >=2.0
# LLM # LLM
anthropic>=0.39.0 # Claude API 官方 SDK anthropic>=0.39.0 # Claude API 官方 SDK
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
-- 列出指定 schema 下所有列的元数据 (达梦方言) -- 列出指定 schema 下所有列的元数据 (达梦方言)
-- 调用方:web/core/db_adapter.py → list_columns() -- 调用方:web/core/db_adapter.py → list_columns()
-- 参数:schema 数据库/模式名(绑定参数,由调用方通过 params 传入,勿拼接) -- 参数:schema 数据库/模式名(绑定参数,由调用方通过 params 传入,勿拼接)
-- 说明:达梦的 ALL_TAB_COLUMNS.COMMENTS 列名与 MySQL 的 COLUMN_COMMENT 不同 -- 说明:
-- 达梦的 OWNER 通常为大写 -- - 达梦的 ALL_TAB_COLUMNS.COMMENTS 列名与 MySQL 的 COLUMN_COMMENT 不同
-- - 达梦的 OWNER 通常为大写
-- - 列注释不在 ALL_TAB_COLUMNS 里,要 LEFT JOIN ALL_COL_COMMENTS(Oracle 同源)
-- ============================================================================ -- ============================================================================
SELECT SELECT
c.TABLE_NAME AS table_name, c.TABLE_NAME AS table_name,
...@@ -16,8 +18,12 @@ SELECT ...@@ -16,8 +18,12 @@ SELECT
c.DATA_SCALE AS numeric_scale, c.DATA_SCALE AS numeric_scale,
c.NULLABLE AS is_nullable, c.NULLABLE AS is_nullable,
c.DATA_DEFAULT AS column_default, c.DATA_DEFAULT AS column_default,
c.COMMENTS AS column_comment, cc.COMMENTS AS column_comment,
NULL AS extra NULL AS extra
FROM ALL_TAB_COLUMNS c 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 = ? WHERE c.OWNER = ?
ORDER BY c.TABLE_NAME, c.COLUMN_ID ORDER BY c.TABLE_NAME, c.COLUMN_ID
\ 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
...@@ -2,16 +2,26 @@ ...@@ -2,16 +2,26 @@
-- 列出指定 schema 下所有表 (达梦方言) -- 列出指定 schema 下所有表 (达梦方言)
-- 调用方:web/core/db_adapter.py → list_tables() -- 调用方:web/core/db_adapter.py → list_tables()
-- 参数:schema 数据库/模式名(绑定参数,由调用方通过 params 传入,勿拼接) -- 参数: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 TABLE_NAME, SELECT t.TABLE_NAME AS table_name,
'BASE TABLE' AS TABLE_TYPE, 'BASE TABLE' AS table_type,
NULL AS ENGINE, NULL AS engine,
NUM_ROWS AS TABLE_ROWS, t.NUM_ROWS AS table_rows,
BYTES AS DATA_LENGTH, NULL AS data_length,
0 AS INDEX_LENGTH, 0 AS index_length,
COMMENTS AS TABLE_COMMENT, tc.COMMENTS AS table_comment,
CREATED AS CREATE_TIME, NULL AS create_time,
LAST_DDL_TIME AS UPDATE_TIME NULL AS update_time
FROM ALL_TABLES FROM ALL_TABLES t
WHERE OWNER = UPPER(?) LEFT JOIN ALL_TAB_COMMENTS tc
ORDER BY TABLE_NAME ON tc.OWNER = t.OWNER
\ No newline at end of file AND tc.TABLE_NAME = t.TABLE_NAME
WHERE t.OWNER = UPPER(?)
ORDER BY t.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 出来(与达梦同源——同一类问题同一个修法)
-- ============================================================================
SELECT t.TABLE_NAME AS table_name,
'BASE TABLE' AS table_type,
NULL AS engine,
t.NUM_ROWS AS table_rows,
t.BYTES AS data_length,
0 AS index_length,
tc.COMMENTS AS table_comment,
t.CREATED AS create_time,
t.LAST_DDL_TIME 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(:1)
ORDER BY t.TABLE_NAME
...@@ -124,7 +124,7 @@ class SQLLoader: ...@@ -124,7 +124,7 @@ class SQLLoader:
Args: Args:
name: 模板路径(不含 .sql 后缀),如 "info_schema/list_columns" name: 模板路径(不含 .sql 后缀),如 "info_schema/list_columns"
dialect: 数据库方言 "mysql" / "dameng" dialect: 数据库方言 "mysql" / "dameng" / "oracle"
**params: 占位符参数 **params: 占位符参数
Returns: Returns:
...@@ -148,7 +148,7 @@ class SQLLoader: ...@@ -148,7 +148,7 @@ class SQLLoader:
if "." in stem: if "." in stem:
# 可能含 dialect 后缀:list_columns.mysql # 可能含 dialect 后缀:list_columns.mysql
base, _, maybe_dialect = stem.rpartition(".") base, _, maybe_dialect = stem.rpartition(".")
if maybe_dialect in ("mysql", "dameng"): if maybe_dialect in ("mysql", "dameng", "oracle"):
stem = base stem = base
dialect = maybe_dialect dialect = maybe_dialect
results.append({ results.append({
......
...@@ -6,12 +6,14 @@ ...@@ -6,12 +6,14 @@
-- ${type_col} 证件类型字段名(自动加引号) -- ${type_col} 证件类型字段名(自动加引号)
-- ${no_col} 证件号字段名(自动加引号) -- ${no_col} 证件号字段名(自动加引号)
-- ${min_count} 阈值(默认 2),出现次数 >= 该值才纳入疑似重复 -- ${min_count} 阈值(默认 2),出现次数 >= 该值才纳入疑似重复
-- 注意:所有标识符统一走 ${... | quote} filter,跨 MySQL / 达梦 / Oracle 方言
-- MySQL → 反引号,达梦/Oracle → 双引号
-- ============================================================================ -- ============================================================================
SELECT `${type_col}` AS id_type, `${no_col}` AS id_no, COUNT(*) AS dup_count SELECT ${type_col | quote} AS id_type, ${no_col | quote} AS id_no, COUNT(*) AS dup_count
FROM `${table}` FROM ${table | quote}
WHERE `${type_col}` IS NOT NULL AND TRIM(`${type_col}`) <> '' WHERE ${type_col | quote} IS NOT NULL AND TRIM(${type_col | quote}) <> ''
AND `${no_col}` IS NOT NULL AND TRIM(`${no_col}`) <> '' AND ${no_col | quote} IS NOT NULL AND TRIM(${no_col | quote}) <> ''
GROUP BY `${type_col}`, `${no_col}` GROUP BY ${type_col | quote}, ${no_col | quote}
HAVING COUNT(*) >= ${min_count} HAVING COUNT(*) >= ${min_count}
ORDER BY dup_count DESC ORDER BY dup_count DESC
LIMIT 200 LIMIT 200
\ No newline at end of file
...@@ -7,11 +7,12 @@ ...@@ -7,11 +7,12 @@
-- ${table} 表名(自动加引号) -- ${table} 表名(自动加引号)
-- ${val_col} 待聚合字段名(自动加引号) -- ${val_col} 待聚合字段名(自动加引号)
-- ${min_count} 阈值(默认 2),出现次数 >= 该值才纳入疑似重复 -- ${min_count} 阈值(默认 2),出现次数 >= 该值才纳入疑似重复
-- 注意:所有标识符统一走 ${... | quote} filter,跨 MySQL / 达梦 / Oracle 方言
-- ============================================================================ -- ============================================================================
SELECT `${val_col}` AS value, COUNT(*) AS dup_count SELECT ${val_col | quote} AS value, COUNT(*) AS dup_count
FROM `${table}` FROM ${table | quote}
WHERE `${val_col}` IS NOT NULL AND TRIM(`${val_col}`) <> '' WHERE ${val_col | quote} IS NOT NULL AND TRIM(${val_col | quote}) <> ''
GROUP BY `${val_col}` GROUP BY ${val_col | quote}
HAVING COUNT(*) >= ${min_count} HAVING COUNT(*) >= ${min_count}
ORDER BY dup_count DESC ORDER BY dup_count DESC
LIMIT 200 LIMIT 200
\ No newline at end of file
...@@ -6,9 +6,10 @@ ...@@ -6,9 +6,10 @@
-- ${type_col} 证件类型字段名(自动加引号) -- ${type_col} 证件类型字段名(自动加引号)
-- ${no_col} 证件号字段名(自动加引号) -- ${no_col} 证件号字段名(自动加引号)
-- ${limit} 抽样上限 -- ${limit} 抽样上限
-- 注意:所有标识符统一走 ${... | quote} filter,跨 MySQL / 达梦 / Oracle 方言
-- ============================================================================ -- ============================================================================
SELECT `${type_col}` AS id_type, `${no_col}` AS id_no SELECT ${type_col | quote} AS id_type, ${no_col | quote} AS id_no
FROM `${table}` FROM ${table | quote}
WHERE `${type_col}` IS NOT NULL AND TRIM(`${type_col}`) <> '' WHERE ${type_col | quote} IS NOT NULL AND TRIM(${type_col | quote}) <> ''
AND `${no_col}` IS NOT NULL AND TRIM(`${no_col}`) <> '' AND ${no_col | quote} IS NOT NULL AND TRIM(${no_col | quote}) <> ''
LIMIT ${limit} LIMIT ${limit}
\ No newline at end of file
...@@ -6,9 +6,10 @@ ...@@ -6,9 +6,10 @@
-- ${col_a} 第 1 字段名(自动加引号) -- ${col_a} 第 1 字段名(自动加引号)
-- ${col_b} 第 2 字段名(自动加引号) -- ${col_b} 第 2 字段名(自动加引号)
-- ${limit} 抽样上限 -- ${limit} 抽样上限
-- 注意:所有标识符统一走 ${... | quote} filter,跨 MySQL / 达梦 / Oracle 方言
-- ============================================================================ -- ============================================================================
SELECT `${col_a}` AS value_a, `${col_b}` AS value_b SELECT ${col_a | quote} AS value_a, ${col_b | quote} AS value_b
FROM `${table}` FROM ${table | quote}
WHERE `${col_a}` IS NOT NULL AND TRIM(`${col_a}`) <> '' WHERE ${col_a | quote} IS NOT NULL AND TRIM(${col_a | quote}) <> ''
AND `${col_b}` IS NOT NULL AND TRIM(`${col_b}`) <> '' AND ${col_b | quote} IS NOT NULL AND TRIM(${col_b | quote}) <> ''
LIMIT ${limit} LIMIT ${limit}
\ No newline at end of file
...@@ -61,7 +61,7 @@ def check_dependencies(): ...@@ -61,7 +61,7 @@ def check_dependencies():
def check_optional(): def check_optional():
"""检查可选依赖(LLM、达梦)""" """检查可选依赖(LLM、达梦、Oracle)"""
info = {} info = {}
try: try:
import anthropic import anthropic
...@@ -77,6 +77,13 @@ def check_optional(): ...@@ -77,6 +77,13 @@ def check_optional():
except ImportError: except ImportError:
info["dmPython"] = "未安装(达梦数据库不可用)" info["dmPython"] = "未安装(达梦数据库不可用)"
logger.info("dmPython 未安装(达梦数据库不可用)") logger.info("dmPython 未安装(达梦数据库不可用)")
try:
import oracledb
info["oracledb"] = f"已安装(v{oracledb.__version__})"
logger.info(f"oracledb 已安装(v{oracledb.__version__},Oracle 数据库可用)")
except ImportError:
info["oracledb"] = "未安装(Oracle 数据库不可用)"
logger.info("oracledb 未安装(Oracle 数据库不可用)")
api_key = os.environ.get("ANTHROPIC_API_KEY") api_key = os.environ.get("ANTHROPIC_API_KEY")
if api_key: if api_key:
info["ANTHROPIC_API_KEY"] = f"已配置({api_key[:8]}...)" info["ANTHROPIC_API_KEY"] = f"已配置({api_key[:8]}...)"
......
...@@ -54,8 +54,11 @@ ...@@ -54,8 +54,11 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="数据库类型"> <el-form-item label="数据库类型">
<el-radio-group v-model="form.db_type"> <el-radio-group v-model="form.db_type" @change="onDbTypeChange">
<el-radio-button label="mysql">MySQL</el-radio-button> <el-radio-button label="mysql">MySQL</el-radio-button>
<el-radio-button label="dameng">达梦 (DM)</el-radio-button>
<!-- Oracle 暂未实装完成,先隐藏;见 docs/WORKLOG.md -->
<!-- <el-radio-button label="oracle">Oracle</el-radio-button> -->
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
...@@ -94,6 +97,8 @@ ...@@ -94,6 +97,8 @@
<el-option label="utf8mb4" value="utf8mb4" /> <el-option label="utf8mb4" value="utf8mb4" />
<el-option label="utf8" value="utf8" /> <el-option label="utf8" value="utf8" />
<el-option label="gbk" value="gbk" /> <el-option label="gbk" value="gbk" />
<el-option label="al32utf8" value="al32utf8" />
<el-option label="zhs16gbk" value="zhs16gbk" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
...@@ -651,6 +656,8 @@ ...@@ -651,6 +656,8 @@
// ── 表单 ── // ── 表单 ──
// steps 现在是 step_id 字符串列表(由后端 /api/steps 动态决定) // steps 现在是 step_id 字符串列表(由后端 /api/steps 动态决定)
// tables 由用户在「数据字典浏览器」里勾选,传给后端做 table_filter // tables 由用户在「数据字典浏览器」里勾选,传给后端做 table_filter
// host/user/password/database 的默认值从 /api/db-defaults 拉取(见 web/configs/db_defaults.yaml),
// 下面这些是 JS 内置兜底值(API 失败 / 配置缺失时使用)
const form = reactive({ const form = reactive({
db_type: 'mysql', db_type: 'mysql',
host: '192.168.20.10', host: '192.168.20.10',
...@@ -669,6 +676,28 @@ ...@@ -669,6 +676,28 @@
const connected = ref(false); const connected = ref(false);
const testResult = ref(null); const testResult = ref(null);
// ── 数据库类型切换:根据 db_type 自动给 port / database 提示默认值 ──
// 各数据库默认端口;切换时若当前端口仍是「某库的默认端口」就跟着切换,
// 用户手动改过的端口(如 3307)不会被覆盖。
// Oracle 的 database 字段填 service_name(如 ORCLPDB1)
const DB_DEFAULT_PORTS = {
mysql: 3306,
dameng: 5236,
oracle: 1521,
};
function onDbTypeChange(newType) {
const oldPort = form.port;
const newPort = DB_DEFAULT_PORTS[newType];
// 只在「当前端口是某个默认端口」时才自动切换;保留用户自定义
if (newPort && Object.values(DB_DEFAULT_PORTS).includes(oldPort)) {
form.port = newPort;
}
if (newType === 'oracle') {
form.database = form.database || 'ORCL';
}
}
watch(() => form.db_type, onDbTypeChange);
// ── 步骤 & 标准 ── // ── 步骤 & 标准 ──
// stepsList 保留作 fallback / required 检查;分组后用 analysisTree 驱动 UI // stepsList 保留作 fallback / required 检查;分组后用 analysisTree 驱动 UI
const stepsList = ref([]); const stepsList = ref([]);
...@@ -1105,6 +1134,24 @@ ...@@ -1105,6 +1134,24 @@
testResult.value = null; testResult.value = null;
} }
// ── 加载数据库连接默认值(来自 web/configs/db_defaults.yaml) ──
// 只覆盖 form 里现有字段,未返回的字段保留 JS 内置兜底
async function loadDbDefaults() {
try {
const r = await fetch('/api/db-defaults');
if (!r.ok) return;
const d = await r.json();
if (d.host !== undefined) form.host = d.host;
if (d.user !== undefined) form.user = d.user;
// password 即便是空串也要写入(前端可能需要"用配置里的密码"语义)
if (d.password !== undefined) form.password = d.password;
if (d.database !== undefined) form.database = d.database;
console.log('[db-defaults] 已应用配置默认值');
} catch (e) {
console.warn('[db-defaults] 加载失败,保留 JS 内置兜底:', e);
}
}
// ── 加载步骤(flat 列表 —— 用于 fallback / required 兜底) ── // ── 加载步骤(flat 列表 —— 用于 fallback / required 兜底) ──
async function loadSteps() { async function loadSteps() {
try { try {
...@@ -1483,6 +1530,7 @@ ...@@ -1483,6 +1530,7 @@
} }
onMounted(() => { onMounted(() => {
loadDbDefaults(); // 先拉默认值(覆盖 form 初始值)
loadSteps(); loadSteps();
loadAnalysisTree(); loadAnalysisTree();
loadStandards(); loadStandards();
...@@ -1509,6 +1557,8 @@ ...@@ -1509,6 +1557,8 @@
testConnection, startJob, cancelJob, resetAndStart, testConnection, startJob, cancelJob, resetAndStart,
downloadReport, downloadReport,
loadStandards, loadStandards,
loadDbDefaults,
onDbTypeChange,
selectedField, selectFieldFilter, clearFieldFilter, selectedField, selectFieldFilter, clearFieldFilter,
}; };
} }
......
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