Commit db3184e0 authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 任务弹窗「历史连接」下拉选择器

- schema.sql 加 connection_preset 表(UNIQUE(db_type, name) + ord 索引)
- ORM models/connection_preset.py 新建;models/__init__.py 导出
- seed.py 加 seed_connection_presets(13 条来自 web2/configs/db_defaults.yaml;硬编码避免引 pyyaml)
- database.init_db() 改造:DB 不存在→schema.sql 全量;DB 存在→Base.metadata.create_all 增量建表 + _migrate_connection_preset_unique() 一次性补 UNIQUE
- routers/connection_presets.py 新增 5 端点:列表(可选 db_type 过滤)/按 id/POST/PUT/DELETE
- app.py 注册 connection_presets_router
- src/api/client.js 加 params query string 支持(GET list 用)
- src/api/connectionPresets.js 新封装 list/get/create/update/delete + presetToForm helper(密码暂不回填)
- TaskModal.vue Tab2 顶部加「历史连接」section:下拉按当前 dbType 过滤,选中后回填 connName/host/port/db/user 字段,schema/表清空等用户测试连接再拉
parent 6edeb386
......@@ -13,6 +13,9 @@ from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker, Session, declarative_base
from web3.backend.config import DB_PATH
from web3.backend._logging import get_logger
logger = get_logger("backend.db")
# declarative_base 单例:所有模型 import 此 Base
Base = declarative_base()
......@@ -60,14 +63,59 @@ def session_scope() -> Session:
# ── 首次启动初始化 ──
def init_db() -> None:
"""若 DB 文件不存在,建表 + 灌种子。已存在则什么都不做。"""
"""DB 不存在 → 跑 schema.sql 全量建表 + seed。
DB 已存在 → 用 SQLAlchemy 补齐缺失的表(增量迁移,不破坏已有数据),
然后 seed 各码表(每个 seed 自己判重,幂等)。
"""
from web3.backend.db import seed
if DB_PATH.exists():
return
schema = Path(__file__).parent / "schema.sql"
sql = schema.read_text(encoding="utf-8")
with engine.begin() as conn:
for stmt in [s.strip() for s in sql.split(";") if s.strip()]:
conn.exec_driver_sql(stmt)
if not DB_PATH.exists():
# 首次建库:跑 schema.sql 全量
schema = Path(__file__).parent / "schema.sql"
sql = schema.read_text(encoding="utf-8")
with engine.begin() as conn:
for stmt in [s.strip() for s in sql.split(";") if s.strip()]:
conn.exec_driver_sql(stmt)
logger.info(f"[init_db] 首次建库完成:{DB_PATH}")
else:
# 已有 DB:增量补齐 ORM 里声明但 DB 里尚未存在的表(如新加的 connection_preset)
# Base.metadata.create_all 只会创建缺失的表,已存在的表不动
Base.metadata.create_all(engine)
# 一次性迁移:补齐 connection_preset 的 UNIQUE(db_type, name) 约束
# 历史背景:connection_preset 最初是手动跑 schema.sql 片段建的(仅 7 张主表里没有它),
# 然后通过 Base.metadata.create_all 增量建出来时只采纳了 ORM 列,丢了 SQL 里的 UNIQUE。
# 这里检测后重建(connection_preset 里的数据可从 web2 yaml 重新灌入,不影响业务表)。
_migrate_connection_preset_unique()
logger.info(f"[init_db] DB 已存在,已执行增量建表检查:{DB_PATH}")
# 灌种子(每个 seed 函数内部判重,可重复调用)
seed.seed_task_groups()
print(f"[init_db] 已建表并 seed:{DB_PATH}")
\ No newline at end of file
seed.seed_connection_presets()
print(f"[init_db] 初始化完成:{DB_PATH}")
def _migrate_connection_preset_unique() -> None:
"""检查 connection_preset 表是否已有 UNIQUE(db_type, name) 约束,没有则重建表。
SQLite 把 UNIQUE 约束实现为同名 unique index,存在 sqlite_master。
"""
with engine.begin() as conn:
# 1. 查 connection_preset 表是否存在
row = conn.exec_driver_sql(
"SELECT name FROM sqlite_master WHERE type='table' AND name='connection_preset'"
).fetchone()
if not row:
return # 表都不在,Base.metadata.create_all 已经建了带 UNIQUE 的版本,无需处理
# 2. 查 UNIQUE 索引是否存在
row = conn.exec_driver_sql(
"SELECT name FROM sqlite_master WHERE type='index' "
"AND tbl_name='connection_preset' AND name='uq_connection_preset_db_type_name'"
).fetchone()
if row:
return # 已经有约束了
# 3. 重建表(数据从 web2 yaml 灌入,所以删了无所谓)
logger.warning("[migrate] connection_preset 缺 UNIQUE 约束,删除并重建")
conn.exec_driver_sql("DROP TABLE connection_preset")
Base.metadata.create_all(engine, tables=[
t for t in Base.metadata.sorted_tables if t.name == "connection_preset"
])
logger.info("[migrate] connection_preset 已用带 UNIQUE 约束的版本重建")
\ No newline at end of file
......@@ -98,4 +98,35 @@ CREATE TABLE violation (
issues_json TEXT,
bad_field_id INTEGER REFERENCES field(id)
);
CREATE INDEX idx_violation_run ON violation(run_id);
\ No newline at end of file
CREATE INDEX idx_violation_run ON violation(run_id);
-- ─────────────────────────────────────────────────────────────
-- 7) connection_preset · 历史数据库连接(任务弹窗「连接方式」上方的下拉选项)
-- ─────────────────────────────────────────────────────────────
-- 任务级 conn_json 还是存 task.conn_json 走原有的字段;这里只存「预设」让 UI 能复用
-- 字段对齐 web2/configs/db_defaults.yaml:
-- id 主键
-- db_type mysql / dameng / oracle
-- name 同 db_type 内唯一(来自 yaml 的 id 字段)
-- display UI 展示名(来自 yaml 的 display 字段)
-- host / port / user / password / database
-- 连接基本信息
-- oracle_client_dir Oracle Instant Client 路径(可空)
-- ord 显示顺序(同 db_type 内从小到大)
-- created_at 建表时间
CREATE TABLE connection_preset (
id INTEGER PRIMARY KEY AUTOINCREMENT,
db_type TEXT NOT NULL,
name TEXT NOT NULL,
display TEXT,
host TEXT NOT NULL,
port INTEGER NOT NULL,
user TEXT NOT NULL,
password TEXT NOT NULL DEFAULT '',
database TEXT NOT NULL DEFAULT '',
oracle_client_dir TEXT,
ord INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(db_type, name)
);
CREATE INDEX idx_connection_preset_db_type ON connection_preset(db_type, ord);
\ No newline at end of file
"""种子数据 —— 首次建库后灌入基础数据
- seed_task_groups():6 个分组(来自 mock 需求里的左侧菜单「校验任务」)
- seed_connection_presets():13 条历史连接(来自 web2/configs/db_defaults.yaml,
硬编码避免引入 pyyaml 依赖;改完需要重启 web3 后端生效)
"""
from __future__ import annotations
import logging
from web3.backend.db.database import session_scope
from web3.backend.models.connection_preset import ConnectionPreset
from web3.backend.models.task_group import TaskGroup
logger = logging.getLogger(__name__)
......@@ -34,4 +37,58 @@ def seed_task_groups() -> None:
return
for name, ord in DEFAULT_TASK_GROUPS:
db.add(TaskGroup(name=name, ord=ord))
logger.info(f"[seed] task_group 新增 {len(DEFAULT_TASK_GROUPS)} 条")
\ No newline at end of file
logger.info(f"[seed] task_group 新增 {len(DEFAULT_TASK_GROUPS)} 条")
# ── 历史连接预设(来自 web2/configs/db_defaults.yaml,13 条) ──
# 字段:
# db_type 内部小写(mysql / dameng / oracle)
# name yaml 的 id 字段(同一 db_type 内唯一,UI 选中后存 task.conn_json 时也带回去)
# display yaml 的 display 字段(UI 下拉展示用)
# host / port / user / password / database / oracle_client_dir
# yaml 对应字段
# ord 同 db_type 内的显示顺序(10/20/... 一组;跨 db_type 各自递增)
DEFAULT_CONNECTION_PRESETS: list[dict] = [
# ── MySQL(11 条)──
{"db_type": "mysql", "name": "smart-build", "display": "智能建造", "host": "192.168.20.10", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "smart-build", "ord": 10},
{"db_type": "mysql", "name": "cd-zhfw-jiangsu", "display": "江苏房屋", "host": "192.168.20.10", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "cd-zhfw-jiangsu", "ord": 20},
{"db_type": "mysql", "name": "cd-sewage", "display": "湖北污水", "host": "192.168.20.11", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "cd-sewage", "ord": 30},
{"db_type": "mysql", "name": "city-sewage", "display": "垃圾", "host": "192.168.20.11", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "city-sewage", "ord": 40},
{"db_type": "mysql", "name": "farmhouse", "display": "农房工匠", "host": "192.168.20.11", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "farmhouse", "ord": 50},
{"db_type": "mysql", "name": "hbzdmhxm", "display": "重大项目", "host": "192.168.20.11", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "hbzdmhxm", "ord": 60},
{"db_type": "mysql", "name": "old_community", "display": "老旧小区", "host": "192.168.20.11", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "old_community", "ord": 70},
{"db_type": "mysql", "name": "traditional-villages","display": "传统村落", "host": "192.168.20.11", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "traditional-villages","ord": 80},
{"db_type": "mysql", "name": "community-embedded", "display": "智慧社区", "host": "192.168.20.10", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "community-embedded", "ord": 90},
{"db_type": "mysql", "name": "construction", "display": "建筑业", "host": "192.168.20.10", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "construction", "ord": 100},
{"db_type": "mysql", "name": "retailers", "display": "楚凤家政", "host": "192.168.20.10", "port": 3306, "user": "root", "password": "Cd@85617711", "database": "retailers", "ord": 110},
# ── 达梦 DM(1 条)──
{"db_type": "dameng", "name": "sanjin", "display": "三金系统", "host": "192.168.20.24", "port": 5236, "user": "HGWXZJ", "password": "HGwxzj2026", "database": "HGWXZJ", "ord": 10},
# ── Oracle(1 条)──
{"db_type": "oracle", "name": "huanggang-gjj", "display": "黄冈公积金", "host": "192.168.20.196", "port": 1521, "user": "GJJ80", "password": "GJJ80_Hg41RG", "database": "ZFGJJ", "ord": 10,
"oracle_client_dir": "D:\\Oracle\\instantclient-basic-windows.x64-12.2.0.1.0\\instantclient_12_2"},
]
def seed_connection_presets() -> None:
"""首次建库时插入 13 条历史连接;已存在则跳过(按 db_type + name 判重)。"""
with session_scope() as db:
existed = db.query(ConnectionPreset).count()
if existed > 0:
logger.info(f"[seed] connection_preset 已有 {existed} 条,跳过")
return
for cfg in DEFAULT_CONNECTION_PRESETS:
db.add(ConnectionPreset(
db_type=cfg["db_type"],
name=cfg["name"],
display=cfg["display"],
host=cfg["host"],
port=int(cfg["port"]),
user=cfg["user"],
password=cfg["password"],
database=cfg["database"],
oracle_client_dir=cfg.get("oracle_client_dir"),
ord=cfg["ord"],
))
logger.info(f"[seed] connection_preset 新增 {len(DEFAULT_CONNECTION_PRESETS)} 条")
......@@ -2,7 +2,8 @@
每个文件对应 schema.sql 里的一张表。
"""
from .connection_preset import ConnectionPreset
from .task import Task
from .task_group import TaskGroup
__all__ = ["Task", "TaskGroup"]
\ No newline at end of file
__all__ = ["ConnectionPreset", "Task", "TaskGroup"]
\ No newline at end of file
"""connection_preset ORM 模型
对应 schema.sql 里的:
CREATE TABLE connection_preset (
id, db_type, name, display, host, port, user, password, database,
oracle_client_dir, ord, created_at,
UNIQUE(db_type, name)
)
设计:
- 「历史连接」表,给任务弹窗「数据连接」Tab 上方的下拉选择器做数据源
- 字段对齐 web2/configs/db_defaults.yaml(db_type/name/display/host/port/user/password/database/oracle_client_dir)
- 种子数据由 seed.py 从 web2 yaml 灌入(13 条)
- 选完预设只把连接字段回填到 TaskModal 的 form,不直接改 task 表(task.conn_json 才是最终落库的地方)
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Text, UniqueConstraint
from sqlalchemy.sql import func
from web3.backend.db.database import Base
class ConnectionPreset(Base):
__tablename__ = "connection_preset"
__table_args__ = (
UniqueConstraint("db_type", "name", name="uq_connection_preset_db_type_name"),
)
id = Column(Integer, primary_key=True, autoincrement=True)
db_type = Column(String, nullable=False) # mysql / dameng / oracle
name = Column(String, nullable=False) # yaml 的 id 字段
display = Column(String, nullable=True) # UI 展示名(yaml 的 display 字段)
host = Column(String, nullable=False)
port = Column(Integer, nullable=False)
user = Column(String, nullable=False)
password = Column(String, nullable=False, default="") # 与 task.conn_json 同策略:明文存(内部工具)
database = Column(String, nullable=False, default="")
oracle_client_dir = Column(String, nullable=True) # 仅 Oracle 使用
ord = Column(Integer, nullable=False, default=0) # 显示顺序
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
def to_dict(self) -> dict:
"""序列化为前端友好结构
- 包含全部连接字段,前端选完直接回填 TaskModal
"""
return {
"id": self.id,
"db_type": self.db_type, # 内部小写
"name": self.name,
"display": self.display or self.name,
"host": self.host,
"port": self.port,
"user": self.user,
"password": self.password,
"database": self.database,
"oracle_client_dir": self.oracle_client_dir,
"ord": self.ord,
"created_at": self.created_at.isoformat() if isinstance(self.created_at, datetime) else self.created_at,
}
def __repr__(self) -> str:
return f"<ConnectionPreset id={self.id} {self.db_type}/{self.name} host={self.host}:{self.port}>"
"""历史数据库连接预设 路由
端点:
GET /api/connection-presets?db_type= 列出所有预设(可选按 db_type 过滤,按 ord 升序)
GET /api/connection-presets/{id} 按 id 拿一条
POST /api/connection-presets 新建预设
PUT /api/connection-presets/{id} 更新预设(按 id)
DELETE /api/connection-presets/{id} 删除预设(按 id)
设计:
- 给任务弹窗「数据连接」Tab 上方的「历史连接」下拉选择器做数据源
- 数据来源:seed.py 硬编码 13 条(来自 web2/configs/db_defaults.yaml)
- 字段对齐 yaml:db_type/name/display/host/port/user/password/database/oracle_client_dir/ord
- (db_type, name) 唯一 → 冲突时 409
- 密码字段返回明文(与 task.conn_json 一致;内部工具不做加密)
"""
from __future__ import annotations
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from web3.backend.db.database import get_session
from web3.backend.models.connection_preset import ConnectionPreset
router = APIRouter(prefix="/connection-presets", tags=["connection-presets"])
# ── Pydantic schema ──
class ConnectionPresetOut(BaseModel):
id: int
db_type: str
name: str
display: str
host: str
port: int
user: str
password: str
database: str
oracle_client_dir: Optional[str] = None
ord: int
created_at: str
class Config:
from_attributes = True
class ConnectionPresetCreate(BaseModel):
db_type: str = Field(..., description="mysql / dameng / oracle(小写)")
name: str = Field(..., min_length=1, max_length=64, description="同 db_type 内唯一标识")
display: Optional[str] = Field(None, max_length=128, description="UI 展示名")
host: str = Field(..., min_length=1, max_length=128)
port: int = Field(..., ge=1, le=65535)
user: str = Field(..., min_length=1, max_length=64)
password: str = Field("", description="明文存储(与 task.conn_json 一致)")
database: str = Field("", max_length=64)
oracle_client_dir: Optional[str] = Field(None, max_length=512)
ord: int = Field(0, description="显示顺序")
class ConnectionPresetUpdate(BaseModel):
"""全字段可选 —— 局部更新;password 字段为空字符串表示不更新(避免误清空)"""
display: Optional[str] = None
host: Optional[str] = None
port: Optional[int] = Field(None, ge=1, le=65535)
user: Optional[str] = None
password: Optional[str] = None
database: Optional[str] = None
oracle_client_dir: Optional[str] = None
ord: Optional[int] = None
# ── 路由 ──
@router.get("", response_model=list[ConnectionPresetOut], summary="列出历史连接预设")
def list_connection_presets(
db_type: Optional[str] = Query(None, description="按 db_type 过滤:mysql/dameng/oracle"),
db: Session = Depends(get_session),
) -> list[ConnectionPresetOut]:
q = db.query(ConnectionPreset)
if db_type:
q = q.filter(ConnectionPreset.db_type == db_type)
rows = q.order_by(ConnectionPreset.db_type, ConnectionPreset.ord, ConnectionPreset.id).all()
return [ConnectionPresetOut(**r.to_dict()) for r in rows]
@router.get("/{preset_id}", response_model=ConnectionPresetOut, summary="按 id 拿一条预设")
def get_connection_preset(preset_id: int, db: Session = Depends(get_session)) -> ConnectionPresetOut:
row = db.query(ConnectionPreset).filter(ConnectionPreset.id == preset_id).first()
if not row:
raise HTTPException(status_code=404, detail=f"未找到预设:id={preset_id}")
return ConnectionPresetOut(**row.to_dict())
@router.post("", response_model=ConnectionPresetOut, summary="新建历史连接预设")
def create_connection_preset(
payload: ConnectionPresetCreate,
db: Session = Depends(get_session),
) -> ConnectionPresetOut:
row = ConnectionPreset(
db_type=payload.db_type.strip().lower(),
name=payload.name.strip(),
display=(payload.display or payload.name).strip(),
host=payload.host.strip(),
port=payload.port,
user=payload.user.strip(),
password=payload.password,
database=payload.database.strip(),
oracle_client_dir=payload.oracle_client_dir,
ord=payload.ord,
)
db.add(row)
try:
db.commit()
except IntegrityError:
db.rollback()
raise HTTPException(status_code=409, detail=f"预设已存在:{payload.db_type}/{payload.name}")
db.refresh(row)
return ConnectionPresetOut(**row.to_dict())
@router.put("/{preset_id}", response_model=ConnectionPresetOut, summary="更新历史连接预设")
def update_connection_preset(
preset_id: int,
payload: ConnectionPresetUpdate,
db: Session = Depends(get_session),
) -> ConnectionPresetOut:
row = db.query(ConnectionPreset).filter(ConnectionPreset.id == preset_id).first()
if not row:
raise HTTPException(status_code=404, detail=f"未找到预设:id={preset_id}")
# 仅更新提供的字段
data = payload.model_dump(exclude_unset=True)
for k, v in data.items():
setattr(row, k, v)
db.commit()
db.refresh(row)
return ConnectionPresetOut(**row.to_dict())
@router.delete("/{preset_id}", status_code=204, summary="删除历史连接预设")
def delete_connection_preset(preset_id: int, db: Session = Depends(get_session)) -> None:
row = db.query(ConnectionPreset).filter(ConnectionPreset.id == preset_id).first()
if not row:
raise HTTPException(status_code=404, detail=f"未找到预设:id={preset_id}")
db.delete(row)
db.commit()
......@@ -8,14 +8,22 @@
const BASE = '/api'
async function request(path, { method = 'GET', body } = {}) {
async function request(path, { method = 'GET', body, params } = {}) {
// GET 请求把 params 拼到 query string
let fullPath = path
if (params && Object.keys(params).length > 0) {
const qs = new URLSearchParams(
Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '')
).toString()
if (qs) fullPath = `${path}?${qs}`
}
const init = {
method,
headers: { 'Content-Type': 'application/json' },
}
if (body !== undefined) init.body = body
const res = await fetch(`${BASE}${path}`, init)
const res = await fetch(`${BASE}${fullPath}`, init)
if (!res.ok) {
let msg = `HTTP ${res.status}`
try {
......@@ -29,9 +37,9 @@ async function request(path, { method = 'GET', body } = {}) {
}
export const http = {
get: (path) => request(path, { method: 'GET' }),
post: (path, body) => request(path, { method: 'POST', body: JSON.stringify(body ?? {}) }),
put: (path, body) => request(path, { method: 'PUT', body: JSON.stringify(body ?? {}) }),
patch: (path, body) => request(path, { method: 'PATCH', body: JSON.stringify(body ?? {}) }),
delete: (path) => request(path, { method: 'DELETE' }),
get: (path, opts) => request(path, { method: 'GET', ...(opts || {}) }),
post: (path, body) => request(path, { method: 'POST', body: JSON.stringify(body ?? {}) }),
put: (path, body) => request(path, { method: 'PUT', body: JSON.stringify(body ?? {}) }),
patch: (path, body) => request(path, { method: 'PATCH', body: JSON.stringify(body ?? {}) }),
delete: (path) => request(path, { method: 'DELETE' }),
}
\ No newline at end of file
/**
* 历史数据库连接预设 API(2026-08-20 接入)
*
* 任务弹窗「数据连接」Tab 上方下拉框:
* 1. GET /api/connection-presets?db_type=xxx 拉预设列表
* 2. 用户选中某条 → 把连接字段回填到 TaskModal form
*
* 后端 db_type 是小写(mysql/dameng/oracle),前端 dbType 是中文 label(MySQL/Oracle/达梦 DM),
* 这里用 DB_TYPE_MAP 双向转换。
*/
import { http } from './client'
// 前端中文 label → 后端小写(与 connections.js 保持一致)
const DB_TYPE_MAP = {
'MySQL': 'mysql',
'Oracle': 'oracle',
'达梦 DM': 'dameng',
}
// 后端小写 → 前端中文 label(用于选中预设后把 form.dbType 切回去)
const REVERSE_DB_TYPE_MAP = Object.fromEntries(
Object.entries(DB_TYPE_MAP).map(([k, v]) => [v, k])
)
/**
* 列出预设(可选按 db_type 过滤)
* @param {string} [dbTypeLabel] - 前端中文 label('MySQL' / 'Oracle' / '达梦 DM'),传空=全部
*/
export async function listConnectionPresets(dbTypeLabel) {
const params = {}
if (dbTypeLabel) {
params.db_type = DB_TYPE_MAP[dbTypeLabel] || dbTypeLabel
}
return http.get('/connection-presets', { params })
}
/**
* 按 id 拿一条预设
*/
export function getConnectionPreset(id) {
return http.get(`/connection-presets/${id}`)
}
/**
* 新建预设
*/
export function createConnectionPreset(payload) {
return http.post('/connection-presets', payload)
}
/**
* 更新预设
*/
export function updateConnectionPreset(id, payload) {
return http.put(`/connection-presets/${id}`, payload)
}
/**
* 删除预设
*/
export function deleteConnectionPreset(id) {
return http.delete(`/connection-presets/${id}`)
}
/**
* 把后端返回的预设记录转成 TaskModal form 字段
* - db_type: 后端小写 → 前端中文 label
* - port: number → 字符串(前端 form 是字符串)
* - 不回填 password(安全考虑 —— 编辑场景一样,让用户重输;后端任务表密码兜底)
*/
export function presetToForm(preset) {
return {
dbType: REVERSE_DB_TYPE_MAP[preset.db_type] || preset.db_type,
connName: preset.display || preset.name,
host: preset.host || '',
port: preset.port != null ? String(preset.port) : '',
db: preset.database || '',
user: preset.user || '',
password: '', // 不回填密码(与编辑回填策略一致)
jdbcParams: '', // 预设里没有这个字段
// schema/table 不回填 —— 表结构会变,让用户重新「测试连接」拉
}
}
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