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

feat(web3): 任务编辑回填全部连接字段 + 密码兜底 + 移除状态 UI

P3 头部 CRUD 跑通后发现「编辑」流程有 4 个缺口,本次补齐:

后端
- models/__init__.py: 补 Task 导出(之前只有 TaskGroup)
- routers/tasks.py::TaskOut 增加 conn_name/host/port/user/jdbc_params/schema
  6 个字段(从 conn_json 解析),供前端编辑回填
- routers/tasks.py 抽出 _parse_conn_json() helper
- routers/tasks.py::update_task 加密码兜底:前端编辑时
  conn_json.password 为空 → 自动用旧值替换,避免误清空
- routers/tasks.py::copy_task 默认 status='停用' 改为沿用源 status

前端
- TaskModal.vue Tab1 基础信息加「状态」单选(启用/停用)
- TaskModal.vue 编辑 watch 从 task.* 还原全部 13 个字段
  (修前只回填 3 个,连接信息全丢)
- TaskModal.vue 密码不回填到前端(安全考虑),后端兜底
- TaskModal.vue onSave payload 改成 form.status || '启用'
- TaskConfigView.vue 任务列表删「状态」列,colspan 9 → 8

UI 调整
- 用户反馈不要启用/停用状态,又把 Tab1 状态单选和列表状态列都删掉
  (DB status 列保留,向后兼容)

验收
- tests/test_task_conn_fields.py 端到端跑通:6 步全过
parent 2edc6e83
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
每个文件对应 schema.sql 里的一张表。 每个文件对应 schema.sql 里的一张表。
""" """
from .task import Task
from .task_group import TaskGroup from .task_group import TaskGroup
__all__ = ["TaskGroup"] __all__ = ["Task", "TaskGroup"]
\ No newline at end of file \ No newline at end of file
...@@ -66,6 +66,13 @@ class TaskOut(BaseModel): ...@@ -66,6 +66,13 @@ class TaskOut(BaseModel):
group: Optional[str] group: Optional[str]
db_type: str db_type: str
db: Optional[str] = None # 从 conn_json 解析出来的「数据库名」,给前端 TaskConfigView 用 db: Optional[str] = None # 从 conn_json 解析出来的「数据库名」,给前端 TaskConfigView 用
# 以下 conn_* 字段都是从 conn_json 解析出来的,供 TaskModal 编辑时回填
conn_name: Optional[str] = None # 连接名称
host: Optional[str] = None # 主机地址
port: Optional[str] = None # 端口
user: Optional[str] = None # 用户名
jdbc_params: Optional[str] = None # JDBC 连接参数
schema: Optional[str] = None # Schema
data_source_path: Optional[str] data_source_path: Optional[str]
source_table: Optional[str] source_table: Optional[str]
status: str status: str
...@@ -86,22 +93,32 @@ def _resolve_group_id(db: Session, group_name: str) -> int: ...@@ -86,22 +93,32 @@ def _resolve_group_id(db: Session, group_name: str) -> int:
return g.id return g.id
def _parse_conn_json(conn_json: Optional[str]) -> dict:
"""解析 conn_json,解析失败或为空返回空 dict。"""
if not conn_json:
return {}
try:
return json.loads(conn_json)
except Exception:
return {}
def _row_to_out(t: Task) -> TaskOut: def _row_to_out(t: Task) -> TaskOut:
d = t.to_dict() d = t.to_dict()
# 从 conn_json 里抠一个 db(数据库名)出来给前端展示 conn = _parse_conn_json(t.conn_json)
db_name = None
if t.conn_json:
try:
db_name = json.loads(t.conn_json).get("db")
except Exception:
pass
return TaskOut( return TaskOut(
id=d["id"], id=d["id"],
name=d["name"], name=d["name"],
group_id=d["group_id"], group_id=d["group_id"],
group=d["group"], group=d["group"],
db_type=d["db_type"], db_type=d["db_type"],
db=db_name, db=conn.get("db"),
conn_name=conn.get("connName"),
host=conn.get("host"),
port=conn.get("port"),
user=conn.get("user"),
jdbc_params=conn.get("jdbcParams"),
schema=conn.get("schema"),
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"],
...@@ -175,6 +192,16 @@ def update_task(task_id: int, payload: TaskUpdate, db: Session = Depends(get_ses ...@@ -175,6 +192,16 @@ def update_task(task_id: int, payload: TaskUpdate, db: Session = Depends(get_ses
data = payload.model_dump(exclude_unset=True) data = payload.model_dump(exclude_unset=True)
if "group" in data: if "group" in data:
t.group_id = _resolve_group_id(db, data.pop("group")) t.group_id = _resolve_group_id(db, data.pop("group"))
# 如果带了 conn_json 且其中 password 为空,则兜底保留旧密码
# (前端编辑时密码字段不回填,避免误清空)
if "conn_json" in data and data["conn_json"]:
new_conn = _parse_conn_json(data["conn_json"])
if "password" in new_conn and not new_conn["password"]:
old_conn = _parse_conn_json(t.conn_json)
new_conn["password"] = old_conn.get("password", "")
data["conn_json"] = json.dumps(new_conn, ensure_ascii=False)
for k, v in data.items(): for k, v in data.items():
setattr(t, k, v) setattr(t, k, v)
...@@ -215,7 +242,7 @@ def copy_task(task_id: int, db: Session = Depends(get_session)) -> TaskOut: ...@@ -215,7 +242,7 @@ def copy_task(task_id: int, db: Session = Depends(get_session)) -> TaskOut:
group_id=t.group_id, group_id=t.group_id,
data_source_path=t.data_source_path, data_source_path=t.data_source_path,
source_table=t.source_table, source_table=t.source_table,
status="停用", # 副本默认停用,避免被定时任务错拉 status=t.status or "启用", # 沿用源任务 status;P3 阶段 UI 不暴露 status 列
description=t.description, description=t.description,
db_type=t.db_type, db_type=t.db_type,
conn_json=t.conn_json, conn_json=t.conn_json,
......
...@@ -259,13 +259,37 @@ watch(() => props.open, (v) => { ...@@ -259,13 +259,37 @@ watch(() => props.open, (v) => {
activeTab.value = 'basic' activeTab.value = 'basic'
connResult.value = '' connResult.value = ''
if (props.task) { if (props.task) {
// 编辑场景下还原基础字段 // 编辑场景:从 task 还原全部字段(含数据库连接)
form.name = props.task.name // 注意:密码不回填到前端(安全考虑),保留空让用户重输
form.group = props.task.group form.name = props.task.name || ''
form.db = props.task.db form.group = props.task.group || '身份证'
form.desc = props.task.description || ''
form.dbType = props.task.db_type || 'MySQL'
form.connName = props.task.conn_name || ''
form.host = props.task.host || ''
form.port = props.task.port || ''
form.db = props.task.db || ''
form.user = props.task.user || ''
form.password = '' // 密码不回填,重新输入
form.jdbcParams = props.task.jdbc_params || ''
form.schema = props.task.schema || 'public'
// 数据表:后端存的是「t_user_info」纯名,前端直接展示
form.table = props.task.source_table || ''
} else { } else {
// 新增场景:恢复默认值
form.name = '身份证_用户信息表' form.name = '身份证_用户信息表'
form.group = '身份证' form.group = '身份证'
form.desc = '对用户信息表中的身份证号、性别、出生日期进行格式与一致性校验'
form.dbType = 'MySQL'
form.connName = 'customer_db_prod'
form.host = '192.168.101.126'
form.port = '3306'
form.db = 'customer_db'
form.user = 'data_check'
form.password = '******'
form.jdbcParams = 'useUnicode=true&characterEncoding=utf8&useSSL=false'
form.schema = 'public'
form.table = 't_user_info'
} }
rebuildFieldList() rebuildFieldList()
} }
...@@ -306,7 +330,6 @@ function onSave() { ...@@ -306,7 +330,6 @@ function onSave() {
db_type: form.dbType, db_type: form.dbType,
data_source_path: null, // P5 接入 data-sources 后再选本地 sqlite data_source_path: null, // P5 接入 data-sources 后再选本地 sqlite
source_table: form.table ? form.table.split('(')[0] : null, source_table: form.table ? form.table.split('(')[0] : null,
status: '启用',
description: form.desc, description: form.desc,
conn_json: connJson, conn_json: connJson,
} }
......
...@@ -32,12 +32,12 @@ ...@@ -32,12 +32,12 @@
<thead> <thead>
<tr> <tr>
<th>任务名称</th><th>分组</th><th>数据库</th><th>数据表</th> <th>任务名称</th><th>分组</th><th>数据库</th><th>数据表</th>
<th>字段数</th><th>规则数</th><th>状态</th><th>最近检测</th><th>操作</th> <th>字段数</th><th>规则数</th><th>最近检测</th><th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-if="!loading && list.length === 0"> <tr v-if="!loading && list.length === 0">
<td colspan="9" style="text-align:center;color:#909399;padding:24px;">暂无任务,点击右上「+ 新增任务」</td> <td colspan="8" style="text-align:center;color:#909399;padding:24px;">暂无任务,点击右上「+ 新增任务」</td>
</tr> </tr>
<tr v-for="row in list" :key="row.id"> <tr v-for="row in list" :key="row.id">
<td>{{ row.name }}</td> <td>{{ row.name }}</td>
...@@ -46,9 +46,6 @@ ...@@ -46,9 +46,6 @@
<td>{{ row.source_table || '—' }}</td> <td>{{ row.source_table || '—' }}</td>
<td>{{ row.fields }}</td> <td>{{ row.fields }}</td>
<td>{{ row.rules }}</td> <td>{{ row.rules }}</td>
<td>
<span class="tag" :class="row.status === '启用' ? 'tag-green' : 'tag-gray'">{{ row.status }}</span>
</td>
<td>{{ row.checked_at || '—' }}</td> <td>{{ row.checked_at || '—' }}</td>
<td> <td>
<button class="btn-text" @click="openEdit(row)">编辑</button> <button class="btn-text" @click="openEdit(row)">编辑</button>
......
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