Commit 5db40025 authored by Data Governance Dev's avatar Data Governance Dev

feat(web): 数据库类型切换自动套用预设 + 杀 8765 脚本

## db_defaults 三套化

用户反馈:「@db_defaults.yaml 现在这个文件里面由三套数据库配置,
现在页面要改成切换数据库类型同时也要切换默认IP,端口,账户密码和数据库」

### 改动
- web/configs/db_defaults.yaml —— 旧单 dict → 新三键 dict
  (mysql / dameng / oracle 各一套,含 port 字段;文件已在 .gitignore 安全)
- web/api/routes.py /api/db-defaults —— 返回三套 JSON;
  兼容旧格式单 dict(用 _flat 包裹,前端识别后只填 mysql)
- web/static/index.html —— 加 dbDefaults ref + DB_FALLBACK_PRESETS 兜底
  + _applyPreset(t);onDbTypeChange 之前只切 port(保留用户自定义),
  现在无条件套用预设全部 6 个字段(host/port/user/password/database/oracle_client_dir);
  loadDbDefaults 先缓存三套再按当前 db_type 套用

## scripts/kill_web.{sh,bat}

杀掉 web 服务(默认端口 8765,可自定义):
- kill_web.sh —— Git Bash + Linux/macOS 通用(netstat.exe / lsof 自动分支)
- kill_web.bat —— Windows 原生 cmd 兼容

退出码:
  0 杀掉占用进程成功 / 端口空闲
  1 端口已空闲(无需处理)
  2 杀进程失败

## 验证
- curl /api/db-defaults 返回三套预设(端口 3306/5236/1521)✓
- kill_web.sh 空闲端口返 1 / 占用端口返 0(实测杀 18999 PID 23160)✓
- 静态资源 200 ✓
- JS 结构平衡 ✓

## 踩坑
- API 旧格式兼容:YAML 老结构(单 dict 平铺)→ 检测 value 不是 dict 时用 _flat 包裹
- oracle_client_dir 仅 Oracle 有效,三套配置统一保留 key 占位
- password 空串语义:'password' in preset 判定(保持与之前一致)
parent 98604b82
......@@ -5383,3 +5383,70 @@ Total: 28 steps # 4 基础 + 24 indicator checkbox
- 静默失败是 UX 大忌。要么明确报错让用户看到,要么确保 status 反映实际情况
- **状态**:与本次「去掉 partial/full radio」一起提交到下一次 commit
## 2026-08-13 · 数据库类型切换自动套用预设(db_defaults 三套化)
> 用户反馈:「@db_defaults.yaml 现在这个文件里面由三套数据库配置,
> 现在页面要改成切换数据库类型同时也要切换默认IP,端口,账户密码和数据库,
> 改成三套配置」
### 1. 设计:YAML 按 db_type 分键 + API 返回三套 + 前端缓存切换
**YAML 结构**([web/configs/db_defaults.yaml](web/configs/db_defaults.yaml))
```yaml
db_defaults:
mysql: { host, port: 3306, user, password, database, oracle_client_dir }
dameng: { host, port: 5236, user, password, database, oracle_client_dir }
oracle: { host, port: 1521, user, password, database, oracle_client_dir }
```
- 三套独立保存,每套自带 port(之前是前端按 db_type 写死 3306/5236/1521)
- 旧结构(单 dict 平铺)API 端有 fallback:返回 `{_flat: {...}}` → 前端当成 mysql 套配置
- 文件已在 `.gitignore`(`web/configs/db_defaults.yaml`),填真实密码安全
**API 端点**([web/api/routes.py:53-90](web/api/routes.py#L53-L90))
- `GET /api/db-defaults` 返回:
```json
{
"mysql": {"host": "...", "port": 3306, ...},
"dameng": {"host": "...", "port": 5236, ...},
"oracle": {"host": "...", "port": 1521, ...}
}
```
- 新格式:每个 db_type 一套,只取前端关心的 6 个字段
- 旧格式兼容:单 dict 平铺 → 用 `_flat` 包裹,前端识别后只填 mysql
**前端切换逻辑**([web/static/index.html:1178-1201](web/static/index.html#L1178-L1201))
- 加 `dbDefaults = ref({})` 缓存三套预设(`loadDbDefaults()` 时填)
- 加 `DB_FALLBACK_PRESETS` JS 内置兜底(API 失败时仍可工作)
- `_applyPreset(t)`:从 `dbDefaults.value[t]` 或 fallback 取预设,覆写 form 6 个字段
- `onDbTypeChange(newType)`:之前只切 port(保留用户自定义),现在无条件套用预设全部 6 个字段
- host / port / user / password / database / oracle_client_dir 同步切换
- `password` 即便是空串也要写入(保证"用配置文件密码"语义一致)
### 2. 关键改动
| 文件 | 改动 |
|---|---|
| `web/configs/db_defaults.yaml` | 旧单 dict → 新三键 dict;保留 .gitignore 安全 |
| `web/api/routes.py` | `/api/db-defaults` 返回三套;兼容旧格式 |
| `web/static/index.html` | `dbDefaults` ref + `DB_FALLBACK_PRESETS` + `_applyPreset`;`onDbTypeChange` 切全部 6 字段;`loadDbDefaults` 缓存三套 |
### 3. 验证
- `curl /api/db-defaults` 返回三套预设,端口分别 3306 / 5236 / 1521 ✓
- 静态资源 `index.html` / `style.css` 加载 200 ✓
- JS 结构平衡:331 对 {},767 对 (),146 对 [] ✓
- 浏览器实测:手动切换「MySQL / 达梦 / Oracle」radio,host/port/user/password/database 5 个字段全部跟着切 ✓
### 4. 踩坑
- **API 旧格式兼容**:YAML 文件可能还没切到新结构。后端检测 `db_defaults` 的 value
不是 dict → 当成老格式单套;前端识别 `_flat` 字段时只填 mysql。这样老 YAML 文件
不改也能跑(虽然只有一套配置可用)
- **oracle_client_dir 是 oracle-only 字段**:mysql/dameng 套配置不需要这个字段,
但为了统一 schema 三个 db_type 都保留 key 占位(值留空)。前端 `_applyPreset`
用 `'oracle_client_dir' in preset` 判定写入,不强制要求 key 存在
- **`password` 空串语义**:与之前 `if (d.password !== undefined)` 保持一致,
即便 YAML 里留空也要写入(让 form.password = '' 显式生效)。如果某 db_type
YAML 没 password 字段,旧逻辑会保留 JS 兜底空串;新逻辑也保留同样行为
@echo off
REM ── 杀掉 web 服务(端口 8765)──
REM 适用:Windows 原生 cmd / PowerShell
REM
REM 用法:
REM scripts\kill_web.bat REM 默认杀 8765
REM scripts\kill_web.bat 9000 REM 自定义端口
REM
REM 退出码(通过 errorlevel):
REM 0 杀掉占用进程成功 / 端口空闲
REM 1 端口已空闲(无需处理)
REM 2 杀进程失败
setlocal enabledelayedexpansion
set "PORT=%~1"
if "%PORT%"=="" set "PORT=8765"
echo [kill_web] 目标端口: %PORT%
REM ── 1. 找占用端口的 PID ──
set "PIDS="
for /f "tokens=5" %%P in ('netstat -ano ^| findstr ":%PORT% " ^| findstr "LISTENING"') do (
if not defined PIDS (
set "PIDS=%%P"
) else (
set "PIDS=!PIDS! %%P"
)
)
if "%PIDS%"=="" (
echo [kill_web] 端口 %PORT% 已空闲,无需处理
exit /b 1
)
echo [kill_web] 占用 %PORT% 的 PID: %PIDS%
REM ── 2. 强杀 ──
set "KILL_OK=0"
for %%P in (%PIDS%) do (
echo [kill_web] -^> 杀 PID %%P
taskkill /F /PID %%P
if !errorlevel! equ 0 set /a KILL_OK+=1
)
REM ── 3. 二次确认 ──
timeout /t 1 /nobreak > nul
set "REMAINING="
for /f "tokens=5" %%P in ('netstat -ano ^| findstr ":%PORT% " ^| findstr "LISTENING"') do (
set "REMAINING=%%P"
goto :check_remain
)
:check_remain
if "%REMAINING%"=="" (
echo [kill_web] 端口 %PORT% 已释放(成功杀掉 %KILL_OK% 个进程)
exit /b 0
) else (
echo [kill_web] 端口 %PORT% 仍有进程占用: %REMAINING% 1>&2
exit /b 2
)
#!/usr/bin/env bash
# ── 杀掉 web 服务(端口 8765) ──
# 适用:Windows + Git Bash(netstat.exe + taskkill.exe)
# Linux/macOS(lsof + kill)—— 已自动分支
#
# 用法:
# bash scripts/kill_web.sh # 默认杀 8765
# bash scripts/kill_web.sh 9000 # 自定义端口
#
# 退出码:
# 0 端口空闲 / 已杀掉占用进程
# 1 端口未占用(无需处理)
# 2 杀进程失败
set -u
PORT="${1:-8765}"
echo "[kill_web] 目标端口: $PORT"
# ── 1. 找到占用端口的 PID(跨平台分支) ──
if command -v netstat.exe > /dev/null 2>&1; then
# ── Windows(Git Bash 调用 netstat.exe) ──
# netstat -ano 列所有 TCP;过滤 ":$PORT " + LISTENING
PIDS=$(netstat.exe -ano 2>/dev/null \
| grep ":$PORT " \
| grep "LISTENING" \
| awk '{print $NF}' \
| sort -u)
elif command -v lsof > /dev/null 2>&1; then
# ── Linux / macOS ──
PIDS=$(lsof -ti tcp:$PORT 2>/dev/null | sort -u)
else
echo "[kill_web] 找不到 netstat.exe 或 lsof,无法定位 PID" >&2
exit 2
fi
if [ -z "${PIDS:-}" ]; then
echo "[kill_web] 端口 $PORT 已空闲,无需处理"
exit 1
fi
echo "[kill_web] 占用 $PORT 的 PID: $(echo "$PIDS" | tr '\n' ' ')"
# ── 2. 杀进程(跨平台分支) ──
KILL_OK=0
for PID in $PIDS; do
echo "[kill_web] → 杀 PID $PID"
if command -v taskkill.exe > /dev/null 2>&1; then
# Windows 强杀
taskkill.exe //F //PID "$PID" 2>&1 | sed 's/^/ /'
RC=${PIPESTATUS[0]}
else
# POSIX 先 SIGTERM 再 SIGKILL
kill "$PID" 2>/dev/null && sleep 1
kill -9 "$PID" 2>/dev/null
RC=$?
fi
if [ "$RC" -eq 0 ]; then
KILL_OK=$((KILL_OK + 1))
else
echo "[kill_web] PID $PID 杀失败(退出码 $RC)" >&2
fi
done
# ── 3. 二次确认端口是否真的释放 ──
sleep 1
if command -v netstat.exe > /dev/null 2>&1; then
REMAINING=$(netstat.exe -ano 2>/dev/null \
| grep ":$PORT " \
| grep "LISTENING" \
| awk '{print $NF}' \
| sort -u)
else
REMAINING=$(lsof -ti tcp:$PORT 2>/dev/null | sort -u)
fi
if [ -z "${REMAINING:-}" ]; then
echo "[kill_web] 端口 $PORT 已释放(成功杀掉 $KILL_OK 个进程)"
exit 0
else
echo "[kill_web] 端口 $PORT 仍有进程占用: $REMAINING" >&2
exit 2
fi
......@@ -48,8 +48,9 @@ async def health():
# ── 数据库连接默认值 ──
# 从 web/configs/db_defaults.yaml 读取,供前端表单初始渲染使用
# 从 web/configs/db_defaults.yaml 读取,供前端表单初始渲染 + 切换 db_type 时套用对应预设
# 文件不存在或解析失败时返回空 dict,前端保留 JS 内置兜底值
# 2026-08-13:按 db_type 分三套配置(mysql / dameng / oracle),切换数据库类型时自动套用
@router.get("/db-defaults")
async def db_defaults():
import yaml # 延迟导入,yaml 是可选依赖的隐性兜底
......@@ -63,13 +64,28 @@ async def db_defaults():
logger.warning(f"db_defaults.yaml 解析失败: {e}")
return {}
defaults = raw.get("db_defaults", {})
# 仅返回前端表单关心的字段,避免泄露无关配置
# 2026-08-13:oracle_client_dir 加入预填(仅 Oracle 有效,留空走 thin 模式)
# 旧结构兼容(2026-08-13 之前是单 dict):
# 如果顶层 db_defaults 的 value 不是 dict,就当成「老格式单套」包成 {mysql: ...}
if not isinstance(defaults, dict) or any(
not isinstance(v, dict) for v in defaults.values()
):
# 老格式:所有字段平铺在 db_defaults 下;按 db_type 自动猜测套用范围
return {
k: defaults[k]
for k in ("host", "user", "password", "database", "oracle_client_dir")
"_flat": {
k: defaults.get(k)
for k in ("host", "port", "user", "password", "database", "oracle_client_dir")
if k in defaults
}
}
# 新格式:返回三套(每套只取前端关心的字段)
fields = ("host", "port", "user", "password", "database", "oracle_client_dir")
out = {}
for db_type in ("mysql", "dameng", "oracle"):
cfg = defaults.get(db_type)
if not isinstance(cfg, dict):
continue
out[db_type] = {k: cfg[k] for k in fields if k in cfg}
return out
# ── LLM 状态(前端用来显示当前 provider) ──
......
......@@ -1171,25 +1171,32 @@
const connected = ref(false);
const testResult = ref(null);
// ── 数据库类型切换:根据 db_type 自动给 port / database 提示默认值 ──
// 各数据库默认端口;切换时若当前端口仍是「某库的默认端口」就跟着切换,
// 用户手动改过的端口(如 3307)不会被覆盖。
// Oracle 的 database 字段填 service_name(如 ORCLPDB1)
const DB_DEFAULT_PORTS = {
mysql: 3306,
dameng: 5236,
oracle: 1521,
// ── 数据库类型切换:根据 db_type 自动套用对应预设 ──
// 2026-08-13:之前只切 port(保留用户自定义),现在按用户要求:
// 切换 db_type 时同步切 host / port / user / password / database
// 五个字段 + oracle_client_dir(仅 Oracle 有效)。
// dbDefaults 由 loadDbDefaults() 从 /api/db-defaults 拉取并缓存。
const dbDefaults = ref({}); // { mysql: {...}, dameng: {...}, oracle: {...} }
// JS 内置兜底(API 失败 / 配置缺失时使用)
const DB_FALLBACK_PRESETS = {
mysql: { host: '192.168.20.10', port: 3306, user: 'root', password: '', database: 'smart-build', oracle_client_dir: '' },
dameng: { host: '192.168.20.24', port: 5236, user: 'HGWXZJ', password: '', database: 'HGWXZJ', oracle_client_dir: '' },
oracle: { host: '192.168.20.196', port: 1521, user: 'GJJ80', password: '', database: 'ZFGJJ', oracle_client_dir: '' },
};
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';
function _applyPreset(t) {
const preset = dbDefaults.value[t] || DB_FALLBACK_PRESETS[t];
if (!preset) return;
if (preset.host !== undefined) form.host = preset.host;
if (preset.port !== undefined) form.port = preset.port;
if (preset.user !== undefined) form.user = preset.user;
// password 即便是空串也要写入(与之前语义一致)
if ('password' in preset) form.password = preset.password;
if (preset.database !== undefined) form.database = preset.database;
if ('oracle_client_dir' in preset) form.oracle_client_dir = preset.oracle_client_dir;
}
function onDbTypeChange(newType) {
// 切换 db_type 时无条件套用对应预设的 host/port/user/password/database
_applyPreset(newType);
}
watch(() => form.db_type, onDbTypeChange);
......@@ -1999,20 +2006,24 @@
}
// ── 加载数据库连接默认值(来自 web/configs/db_defaults.yaml) ──
// 只覆盖 form 里现有字段,未返回的字段保留 JS 内置兜底
// 2026-08-13:三套配置(mysql / dameng / oracle)。先缓存到 dbDefaults,
// 然后按当前 form.db_type 套用对应预设;如果用户后续切换 db_type,
// onDbTypeChange 会从 dbDefaults 里取对应预设再切换一次。
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;
// 2026-08-13:oracle_client_dir 由配置文件注入(UI 已隐藏输入框)
if (d.oracle_client_dir !== undefined) form.oracle_client_dir = d.oracle_client_dir;
console.log('[db-defaults] 已应用配置默认值');
if (d && d._flat) {
// 旧单 dict 兼容:当成 mysql 套配置
dbDefaults.value = { mysql: d._flat };
console.warn('[db-defaults] 检测到旧格式单套配置,仅 mysql 可用');
} else if (d && typeof d === 'object') {
dbDefaults.value = d;
}
// 按当前 db_type 应用预设(确保初始 form 与 YAML 一致)
_applyPreset(form.db_type);
console.log('[db-defaults] 已加载配置:', Object.keys(dbDefaults.value || {}));
} catch (e) {
console.warn('[db-defaults] 加载失败,保留 JS 内置兜底:', e);
}
......
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