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

feat(web3): Oracle 连接支持 + schemas 列表修复 + start.bat 自动重启

包含三块改动:

1. Oracle Instant Client 路径字段端到端打通
   - 后端 ConnectRequest / TaskOut / DBConfig 已有 oracle_client_dir 字段但前端
     表单缺一个输入框,用户拿不到这个值,测试连接永远走 thin 模式
   - TaskModal 表单加 Oracle Client 输入框(仅 dbType=Oracle 时显示)+ 回填/保存
   - connections.js buildPayload / connectionPresets.js presetToForm 串联字段
   - routers/tasks.py TaskOut 加 oracle_client_dir 给编辑场景用
   - routers/queries.py DBConfig 多传 oracle_client_dir(关键,否则查询时
     还是 thin 模式)

2. Oracle schemas 列表 + database/schema 拆分
   - Oracle list_schemas 改用 ALL_TABLES.DISTINCT OWNER(之前用 all_users
     会列所有用户,大部分没表,选了之后 list_tables 找不到表)
   - list_tables / list_columns 加独立 schema 参数:MySQL/达梦 留空走默认
     (fallback 到 database),Oracle 用前端传的 OWNER。
     之前错位:database=service_name 时被当成 OWNER 查 ALL_TABLES → 0 行
   - ConnectRequest 加 schema: Optional[str] 字段
   - 前端 buildPayload 加 schema: form.schema || form.db

3. start.bat 启动后端前自动 kill 占用 :8767 的老进程
   - 后端没 --reload,改代码后必须手动 taskkill 老进程,否则新进程 bind 失败
     静默退出,start.bat 用户以为改了没生效
   - 现在双击 start.bat 就行:netstat 找 PID → taskkill → 等端口释放 → 起新进程

12 条后端用例全过。
parent 7632f29c
...@@ -23,6 +23,12 @@ class ConnectRequest(BaseModel): ...@@ -23,6 +23,12 @@ class ConnectRequest(BaseModel):
connect_timeout: int = 10 connect_timeout: int = 10
# Oracle 专用:Oracle Instant Client 的 lib_dir(oci.dll 所在目录) # Oracle 专用:Oracle Instant Client 的 lib_dir(oci.dll 所在目录)
oracle_client_dir: Optional[str] = None oracle_client_dir: Optional[str] = None
# 用于 list_tables / list_columns:把「库 / 模式 / OWNER」从 database 拆出来
# - MySQL:database == schema,留空走默认(= database)
# - 达梦:登录用户名 = schema,留空走默认(= database)
# - Oracle:database = service_name(连实例用),schema = OWNER(查 ALL_TABLES 用),
# 二者不同 —— 前端 schema 下拉选中的 OWNER 通过这里传进来
schema: Optional[str] = None
class TestConnectionRequest(ConnectRequest): class TestConnectionRequest(ConnectRequest):
......
...@@ -141,7 +141,10 @@ async def list_schemas(req: TestConnectionRequest): ...@@ -141,7 +141,10 @@ async def list_schemas(req: TestConnectionRequest):
- MySQL = SHOW DATABASES - MySQL = SHOW DATABASES
- 达梦 = SELECT DISTINCT OWNER FROM ALL_TABLES(排除系统用户) - 达梦 = SELECT DISTINCT OWNER FROM ALL_TABLES(排除系统用户)
- Oracle = SELECT username FROM all_users(排除系统用户) - Oracle = SELECT DISTINCT OWNER FROM ALL_TABLES
(不再用 all_users —— 会列出所有用户,里头大部分没表,用户选了之后
list_tables 又找不到表,体验差;ALL_TABLES 会自带权限过滤,列出来的都是
当前用户能 SELECT 的「有表」schema)
""" """
cfg = _build_db_config(req) cfg = _build_db_config(req)
logger.info("─" * 60) logger.info("─" * 60)
...@@ -161,12 +164,11 @@ async def list_schemas(req: TestConnectionRequest): ...@@ -161,12 +164,11 @@ async def list_schemas(req: TestConnectionRequest):
) )
schemas = [r["schema_name"] for r in rows] schemas = [r["schema_name"] for r in rows]
elif cfg.db_type == "oracle": elif cfg.db_type == "oracle":
# 与达梦一致:用 ALL_TABLES.DISTINCT OWNER —— 列出的都是「有表 + 当前用户能 SELECT」的模式
# 之前用 all_users 会列出所有用户(含没表的),用户选了之后 list_tables 找不到表,体验差
# ALL_TABLES 自身就会排除系统表(OWNER 是 SYS/SYSTEM 等),不用再 WHERE 过滤
rows = db.fetchall( rows = db.fetchall(
"SELECT username AS schema_name FROM all_users " "SELECT DISTINCT OWNER AS schema_name FROM ALL_TABLES ORDER BY OWNER"
"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] schemas = [r["schema_name"] for r in rows]
else: else:
...@@ -191,11 +193,15 @@ async def list_tables(req: TestConnectionRequest): ...@@ -191,11 +193,15 @@ async def list_tables(req: TestConnectionRequest):
"""复用 db_adapter.list_tables(),走 info_schema/list_tables.<dialect>.sql 模板""" """复用 db_adapter.list_tables(),走 info_schema/list_tables.<dialect>.sql 模板"""
cfg = _build_db_config(req) cfg = _build_db_config(req)
logger.info("─" * 60) logger.info("─" * 60)
logger.info(f"POST /api/connect/tables {_mask_password(cfg)} schema={req.database!r}") schema = req.schema or req.database
logger.info(f"POST /api/connect/tables {_mask_password(cfg)} schema={schema!r}")
try: try:
with DBConnection(cfg) as db: with DBConnection(cfg) as db:
rows = db.list_tables(req.database) # Oracle:database 是 service_name(连实例),schema 才是 OWNER(查 ALL_TABLES 用)
# 其他方言:database == schema,缺省时(schema 为 None)fallback 到 database 即可
schema = req.schema or req.database
rows = db.list_tables(schema)
logger.info(f"✅ 返回 {len(rows)} 张表") logger.info(f"✅ 返回 {len(rows)} 张表")
logger.info("─" * 60) logger.info("─" * 60)
return {"ok": True, "tables": rows, "count": len(rows)} return {"ok": True, "tables": rows, "count": len(rows)}
...@@ -222,14 +228,15 @@ async def list_columns(req: ListColumnsRequest): ...@@ -222,14 +228,15 @@ async def list_columns(req: ListColumnsRequest):
""" """
cfg = _build_db_config(req) cfg = _build_db_config(req)
logger.info("─" * 60) logger.info("─" * 60)
schema = req.schema or req.database
logger.info( logger.info(
f"POST /api/connect/columns {_mask_password(cfg)} " f"POST /api/connect/columns {_mask_password(cfg)} "
f"schema={req.database!r} table={req.table_name!r}" f"schema={schema!r} table={req.table_name!r}"
) )
try: try:
with DBConnection(cfg) as db: with DBConnection(cfg) as db:
rows = db.list_columns(req.database, req.table_name) rows = db.list_columns(schema, req.table_name)
logger.info(f"✅ 返回 {len(rows)} 列") logger.info(f"✅ 返回 {len(rows)} 列")
logger.info("─" * 60) logger.info("─" * 60)
return {"ok": True, "columns": rows, "count": len(rows)} return {"ok": True, "columns": rows, "count": len(rows)}
......
...@@ -137,6 +137,10 @@ async def run_query(req: RunQueryRequest, db: Session = Depends(get_session)): ...@@ -137,6 +137,10 @@ async def run_query(req: RunQueryRequest, db: Session = Depends(get_session)):
database=conn.get("db") or "", database=conn.get("db") or "",
charset="utf8mb4", charset="utf8mb4",
connect_timeout=10, connect_timeout=10,
# Oracle 专用:编辑时前端回填的 Instant Client 路径(保存进 conn_json)
# 不传 → _ensure_oracle_client 走默认搜索 PATH/注册表/ORACLE_HOME,
# 找不到就退回 thin 模式 → DPY-3010(参考 db_adapter.py:_ensure_oracle_client 注释)
oracle_client_dir=conn.get("oracleClientDir"),
) )
# 5) 编译正则(提前编译,错的话直接返回不要连库) # 5) 编译正则(提前编译,错的话直接返回不要连库)
......
...@@ -104,6 +104,7 @@ class TaskOut(BaseModel): ...@@ -104,6 +104,7 @@ class TaskOut(BaseModel):
user: Optional[str] = None # 用户名 user: Optional[str] = None # 用户名
jdbc_params: Optional[str] = None # JDBC 连接参数 jdbc_params: Optional[str] = None # JDBC 连接参数
schema: Optional[str] = None # Schema schema: Optional[str] = None # Schema
oracle_client_dir: Optional[str] = None # Oracle Instant Client 路径(仅 Oracle 有效)
data_source_path: Optional[str] data_source_path: Optional[str]
source_table: Optional[str] source_table: Optional[str]
status: str status: str
...@@ -235,6 +236,7 @@ def _row_to_out(db: Session, t: Task, *, include_field_list: bool = False) -> Ta ...@@ -235,6 +236,7 @@ def _row_to_out(db: Session, t: Task, *, include_field_list: bool = False) -> Ta
user=conn.get("user"), user=conn.get("user"),
jdbc_params=conn.get("jdbcParams"), jdbc_params=conn.get("jdbcParams"),
schema=conn.get("schema"), schema=conn.get("schema"),
oracle_client_dir=conn.get("oracleClientDir"),
data_source_path=d["data_source_path"], data_source_path=d["data_source_path"],
source_table=d["source_table"], source_table=d["source_table"],
status=d["status"], status=d["status"],
......
...@@ -72,14 +72,15 @@ export function deleteConnectionPreset(id) { ...@@ -72,14 +72,15 @@ export function deleteConnectionPreset(id) {
*/ */
export function presetToForm(preset) { export function presetToForm(preset) {
return { return {
dbType: REVERSE_DB_TYPE_MAP[preset.db_type] || preset.db_type, dbType: REVERSE_DB_TYPE_MAP[preset.db_type] || preset.db_type,
connName: preset.display || preset.name, connName: preset.display || preset.name,
host: preset.host || '', host: preset.host || '',
port: preset.port != null ? String(preset.port) : '', port: preset.port != null ? String(preset.port) : '',
db: preset.database || '', db: preset.database || '',
user: preset.user || '', user: preset.user || '',
password: preset.password || '', // 历史记录里存的密码直接回填 password: preset.password || '', // 历史记录里存的密码直接回填
jdbcParams: '', // 预设里没有这个字段 jdbcParams: '', // 预设里没有这个字段
oracleClientDir: preset.oracle_client_dir || '', // 仅 Oracle 使用;非 Oracle 留空即可
// schema/table 不回填 —— 表结构会变,让用户重新「测试连接」拉 // schema/table 不回填 —— 表结构会变,让用户重新「测试连接」拉
} }
} }
...@@ -27,8 +27,16 @@ function buildPayload(form) { ...@@ -27,8 +27,16 @@ function buildPayload(form) {
user: form.user, user: form.user,
password: form.password, password: form.password,
database: form.db, database: form.db,
// schema 与 database 在 Oracle 里是两个东西:
// database = service_name(连实例用);schema = OWNER(查 ALL_TABLES 用)
// MySQL/达梦:database == schema,留空即可(后端 fallback)
// 不传会让 list_tables/list_columns 用错位,Oracle 永远查不到表
schema: (form.schema || form.db || '').trim() || null,
charset: 'utf8mb4', charset: 'utf8mb4',
connect_timeout: 10, connect_timeout: 10,
// Oracle 专用:Instant Client lib_dir(oci.dll 所在目录)
// 空字符串 → 传给后端为 null,后端走 thin 模式 / 自动搜索
oracle_client_dir: (form.oracleClientDir || '').trim() || null,
} }
} }
......
...@@ -108,6 +108,21 @@ ...@@ -108,6 +108,21 @@
<span class="form-label"><span class="required">*</span>密码</span> <span class="form-label"><span class="required">*</span>密码</span>
<div class="form-control"><input type="password" class="text-input" v-model="form.password" /></div> <div class="form-control"><input type="password" class="text-input" v-model="form.password" /></div>
</div> </div>
<!-- 仅 Oracle:Instant Client 路径(oci.dll 所在目录)。
Oracle 11g 及以下 thin 模式不支持(DPY-3010),必须装 Instant Client 让驱动切 thick。
留空则后端走默认搜索(PATH / 注册表 / ORACLE_HOME)。 -->
<div v-if="form.dbType === 'Oracle'" class="form-row full">
<span class="form-label">Oracle Client</span>
<div class="form-control">
<input
type="text"
class="text-input"
v-model="form.oracleClientDir"
placeholder="如:D:\Oracle\instantclient-basic-windows.x64-12.2.0.1.0\instantclient_12_2(留空走默认搜索)"
/>
<div class="form-hint">thin 模式不支持老 Oracle(11.2 以下),需下载 Instant Client Basic 包解压到本地后填该路径。</div>
</div>
</div>
<div class="form-row full"> <div class="form-row full">
<span class="form-label">连接参数</span> <span class="form-label">连接参数</span>
<div class="form-control"> <div class="form-control">
...@@ -317,6 +332,7 @@ const form = reactive({ ...@@ -317,6 +332,7 @@ const form = reactive({
db: '', db: '',
user: '', user: '',
password: '', password: '',
oracleClientDir: '', // 仅 Oracle 使用:Instant Client lib_dir(oci.dll 所在目录)
jdbcParams: '', jdbcParams: '',
schema: '', schema: '',
table: '', table: '',
...@@ -490,6 +506,8 @@ watch(() => form.dbType, async (newVal) => { ...@@ -490,6 +506,8 @@ watch(() => form.dbType, async (newVal) => {
tableOptions.value = [] tableOptions.value = []
form.schema = '' form.schema = ''
form.table = '' form.table = ''
// Oracle Client 路径是 Oracle 专用,切到 MySQL/达梦 应该清掉,避免脏数据被存进 conn_json
form.oracleClientDir = ''
connResult.value = { ok: null, message: '' } connResult.value = { ok: null, message: '' }
connVerified.value = false // 切 dbType → 旧验证失效,等用户重新测试连接 connVerified.value = false // 切 dbType → 旧验证失效,等用户重新测试连接
selectedPresetId.value = null // 切 dbType 后清掉选中(当前 dbType 的预设列表会刷新) selectedPresetId.value = null // 切 dbType 后清掉选中(当前 dbType 的预设列表会刷新)
...@@ -544,6 +562,7 @@ watch(() => props.open, async (v) => { ...@@ -544,6 +562,7 @@ watch(() => props.open, async (v) => {
form.db = props.task.db || '' form.db = props.task.db || ''
form.user = props.task.user || '' form.user = props.task.user || ''
form.password = '' // 密码不回填(安全),但下面会自动从匹配的预设里补回来 form.password = '' // 密码不回填(安全),但下面会自动从匹配的预设里补回来
form.oracleClientDir = props.task.oracle_client_dir || ''
form.jdbcParams = props.task.jdbc_params || '' form.jdbcParams = props.task.jdbc_params || ''
form.schema = props.task.schema || '' form.schema = props.task.schema || ''
// 数据表:后端存的是「t_user_info」纯名,前端直接展示 // 数据表:后端存的是「t_user_info」纯名,前端直接展示
...@@ -579,6 +598,7 @@ watch(() => props.open, async (v) => { ...@@ -579,6 +598,7 @@ watch(() => props.open, async (v) => {
form.db = '' form.db = ''
form.user = '' form.user = ''
form.password = '' form.password = ''
form.oracleClientDir = ''
form.jdbcParams = '' form.jdbcParams = ''
form.schema = '' form.schema = ''
form.table = '' form.table = ''
...@@ -617,6 +637,7 @@ function onSave() { ...@@ -617,6 +637,7 @@ function onSave() {
db: form.db, db: form.db,
user: form.user, user: form.user,
password: form.password, password: form.password,
oracleClientDir: form.oracleClientDir, // 仅 Oracle 使用;非 Oracle 字段为空字符串
jdbcParams: form.jdbcParams, jdbcParams: form.jdbcParams,
schema: form.schema, schema: form.schema,
dbType: form.dbType, dbType: form.dbType,
......
...@@ -59,6 +59,18 @@ if not exist "web3\node_modules" ( ...@@ -59,6 +59,18 @@ if not exist "web3\node_modules" (
REM -- 确保 backend\logs 目录存在 -- REM -- 确保 backend\logs 目录存在 --
if not exist "web3\backend\logs" mkdir "web3\backend\logs" if not exist "web3\backend\logs" mkdir "web3\backend\logs"
REM -- 先杀占用 :8767 的旧后端(避免新进程 bind 失败、start.bat 静默退出)--
REM 后端没有 --reload,改 backend 代码后必须重启;老进程占着端口时新进程起不来,
REM 健康检查 20s 超时报错,但其实只是端口被占。每次跑 start.bat 先清一遍最稳。
echo Checking for old backend on :8767 ...
set "NEED_WAIT="
for /f "tokens=5" %%P in ('netstat -ano ^| findstr ":8767" ^| findstr LISTENING') do (
echo Killing old backend PID %%P ...
taskkill /F /PID %%P >nul 2>&1
set "NEED_WAIT=1"
)
if defined NEED_WAIT ping -n 2 127.0.0.1 >nul 2>&1
REM -- 启动后端(后台进程,cwd 保持项目根以保证 web3 包可被 import) -- REM -- 启动后端(后台进程,cwd 保持项目根以保证 web3 包可被 import) --
echo Starting backend on :8767 ... echo Starting backend on :8767 ...
start /b "" python -m web3.backend.start > "web3\backend\logs\console.log" 2>&1 start /b "" python -m web3.backend.start > "web3\backend\logs\console.log" 2>&1
......
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