Commit 21ee05b2 authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): task 表加 checked_at + table_comment 元数据列

两个独立功能(同一个表的两列)一起做,因为迁移逻辑都走 init_db 兜底:

1. task.checked_at: TIMESTAMP NULL

   schema.sql / Task ORM 加列

   _migrate_task_checked_at() 在 init_db 里兜底

   routers/queries.py::start_query 末尾:sm_put 后写 task.checked_at = utc_now

   try/except + rollback + warning:元数据写失败不能阻塞查询主流程

   时刻选 /start(不是最后一页 /page):中断也算「最近关注过」

2. task.table_comment: TEXT NULL

   schema.sql / Task ORM 加列

   _migrate_task_table_comment() 兜底

   routers/tasks.py: TaskBase/Update/Out 加字段 + _row_to_out + create_task 写入

   update_task 的 setattr 循环自动覆盖

   list_tasks 接口直接读这列返前端,零查询开销

3. CLAUDE.md「只做查询」边界

   这里写 task 元数据,不是业务数据修改

   业务表(field/rule/connection_preset)一律不 UPDATE

4. 前端 TaskConfigView 现在用 row.table_comment 还没显示(commit 4 才加列)

   formatCheckedAt 已经备好(commit 2),checked_at 写完即可生效
parent 0c10c10a
...@@ -95,6 +95,10 @@ def init_db() -> None: ...@@ -95,6 +95,10 @@ def init_db() -> None:
# 一次性迁移:rule 表加 rule_type / code 列(2026-08-21 扩展规则类型:数值 / 日期) # 一次性迁移:rule 表加 rule_type / code 列(2026-08-21 扩展规则类型:数值 / 日期)
# 老数据全部保持 rule_type='regex',code 留空 → 跟旧逻辑完全等价 # 老数据全部保持 rule_type='regex',code 留空 → 跟旧逻辑完全等价
_migrate_rule_type_and_code() _migrate_rule_type_and_code()
# 一次性迁移:task 表加 checked_at 列(2026-08-21 接 UI「最近检测时间」)
_migrate_task_checked_at()
# 一次性迁移:task 表加 table_comment 列(2026-08-21 接「数据表注释」UI)
_migrate_task_table_comment()
logger.info(f"[init_db] DB 已存在,已执行增量建表检查:{DB_PATH}") logger.info(f"[init_db] DB 已存在,已执行增量建表检查:{DB_PATH}")
# 灌种子(每个 seed 函数内部判重,可重复调用) # 灌种子(每个 seed 函数内部判重,可重复调用)
seed.seed_task_groups() seed.seed_task_groups()
...@@ -177,4 +181,37 @@ def _migrate_rule_type_and_code() -> None: ...@@ -177,4 +181,37 @@ def _migrate_rule_type_and_code() -> None:
) )
if "code" not in cols: if "code" not in cols:
logger.info("[migrate] rule 缺 code 列,补建(默认 NULL,regex 规则用不上)") logger.info("[migrate] rule 缺 code 列,补建(默认 NULL,regex 规则用不上)")
conn.exec_driver_sql("ALTER TABLE rule ADD COLUMN code TEXT") conn.exec_driver_sql("ALTER TABLE rule ADD COLUMN code TEXT")
\ No newline at end of file
def _migrate_task_checked_at() -> None:
"""给 task 表加 checked_at 列(2026-08-21 接上「最近检测时间」UI)。
ORM 里新加了 checked_at,Base.metadata.create_all 不会给已存在的表加列。
SQLite ALTER TABLE ADD COLUMN 不可逆(不能 DROP COLUMN),但加 nullable 列无破坏性。
老任务这一列是 NULL,UI 显示「—」;新查询由 routers/queries.py::start_query 回填。
"""
with engine.begin() as conn:
cols = {
row[1]: row
for row in conn.exec_driver_sql("PRAGMA table_info(task)").fetchall()
}
if "checked_at" not in cols:
logger.info("[migrate] task 缺 checked_at 列,补建(默认 NULL,老任务显示「—」)")
conn.exec_driver_sql("ALTER TABLE task ADD COLUMN checked_at TIMESTAMP")
def _migrate_task_table_comment() -> None:
"""给 task 表加 table_comment 列(2026-08-21 接「数据表注释」UI)。
选表时由 TaskModal 调 list_tables 拿到,写入这列。
任务列表直接读,避免 list_tasks 接口 N+1 查询 list_tables。
"""
with engine.begin() as conn:
cols = {
row[1]: row
for row in conn.exec_driver_sql("PRAGMA table_info(task)").fetchall()
}
if "table_comment" not in cols:
logger.info("[migrate] task 缺 table_comment 列,补建(默认 NULL,老任务「—」)")
conn.exec_driver_sql("ALTER TABLE task ADD COLUMN table_comment TEXT")
\ No newline at end of file
...@@ -41,7 +41,13 @@ CREATE TABLE task ( ...@@ -41,7 +41,13 @@ CREATE TABLE task (
db_type TEXT NOT NULL DEFAULT 'MySQL', db_type TEXT NOT NULL DEFAULT 'MySQL',
conn_json TEXT, conn_json TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 2026-08-21:最近一次检测完成的时间,由 /api/queries/start 在校验结束后回填
-- 老任务这个列是 NULL,UI 显示「—」
checked_at TIMESTAMP,
-- 2026-08-21:数据表的注释,由 TaskModal 选表时从 list_tables 查一次写入
-- 任务列表直接读这列,避免 N+1 查询
table_comment TEXT
); );
CREATE INDEX idx_task_group ON task(group_id); CREATE INDEX idx_task_group ON task(group_id);
......
...@@ -39,6 +39,12 @@ class Task(Base): ...@@ -39,6 +39,12 @@ class Task(Base):
updated_at = Column(DateTime, nullable=False, updated_at = Column(DateTime, nullable=False,
server_default=func.current_timestamp(), server_default=func.current_timestamp(),
onupdate=func.current_timestamp()) onupdate=func.current_timestamp())
# 2026-08-21:最近一次检测时间,由 routers/queries.py::start_query 回填
# 老任务这个列是 NULL(迁移前建库的),UI 显示「—」
checked_at = Column(DateTime, nullable=True)
# 2026-08-21:数据表注释,由 TaskModal 选表时从 list_tables 查一次写入
# 任务列表直接读这列,避免 N+1 查询
table_comment = Column(Text, nullable=True)
# 关联(不强制 eager load,需要时再 query) # 关联(不强制 eager load,需要时再 query)
group = relationship("TaskGroup", lazy="joined") group = relationship("TaskGroup", lazy="joined")
...@@ -60,7 +66,8 @@ class Task(Base): ...@@ -60,7 +66,8 @@ class Task(Base):
"conn_json": self.conn_json, "conn_json": self.conn_json,
"fields": fields_count, "fields": fields_count,
"rules": rules_count, "rules": rules_count,
"checked_at": None, # P6/P7 检测引擎接上后再回填 "checked_at": self.checked_at.isoformat() if isinstance(self.checked_at, datetime) else self.checked_at,
"table_comment": self.table_comment,
"created_at": self.created_at.isoformat() if isinstance(self.created_at, datetime) else self.created_at, "created_at": self.created_at.isoformat() if isinstance(self.created_at, datetime) else self.created_at,
"updated_at": self.updated_at.isoformat() if isinstance(self.updated_at, datetime) else self.updated_at, "updated_at": self.updated_at.isoformat() if isinstance(self.updated_at, datetime) else self.updated_at,
} }
......
...@@ -33,6 +33,7 @@ from __future__ import annotations ...@@ -33,6 +33,7 @@ from __future__ import annotations
import re import re
import time import time
import uuid import uuid
from datetime import datetime, timezone
from typing import Any, Optional from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
...@@ -324,6 +325,18 @@ async def start_query(req: StartQueryRequest, db: Session = Depends(get_session) ...@@ -324,6 +325,18 @@ async def start_query(req: StartQueryRequest, db: Session = Depends(get_session)
) )
sm_put(sess) sm_put(sess)
# 2026-08-21:回写「最近检测时间」—— 任务被启动过查询就算检测行为
# 用户切到任务配置页能看到时间;中途中断也照样记录(代表「最近关注过」)
# 注意:CLAUDE.md「只做查询」针对业务数据;task.checked_at 是任务元数据,与规则语义对齐
try:
task.checked_at = datetime.now(timezone.utc).replace(tzinfo=None)
db.add(task)
db.commit()
except Exception as e:
# 元数据写失败不能让查询接口失败;回滚 + 记 warning
db.rollback()
logger.warning(f"[queries] 回写 task.checked_at 失败(不影响查询):{type(e).__name__}: {e}")
elapsed_ms = (time.monotonic() - started) * 1000 elapsed_ms = (time.monotonic() - started) * 1000
logger.info( logger.info(
f"✅ session={sess.session_id} total_rows={total_rows} total_pages={total_pages} " f"✅ session={sess.session_id} total_rows={total_rows} total_pages={total_pages} "
......
...@@ -62,6 +62,9 @@ class TaskBase(BaseModel): ...@@ -62,6 +62,9 @@ class TaskBase(BaseModel):
status: str = PydField("启用", description="启用 / 停用") status: str = PydField("启用", description="启用 / 停用")
description: Optional[str] = None description: Optional[str] = None
conn_json: Optional[str] = PydField(None, description="UI 数据连接表单的 JSON 字符串") conn_json: Optional[str] = PydField(None, description="UI 数据连接表单的 JSON 字符串")
# 2026-08-21:数据表注释,由前端 TaskModal 选表时从 list_tables 拿到一起提交
# 后端不主动查(避免 list_tasks 接口 N+1 查询 list_tables)
table_comment: Optional[str] = None
class TaskCreate(TaskBase): class TaskCreate(TaskBase):
...@@ -77,6 +80,7 @@ class TaskUpdate(BaseModel): ...@@ -77,6 +80,7 @@ class TaskUpdate(BaseModel):
status: Optional[str] = None status: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
conn_json: Optional[str] = None conn_json: Optional[str] = None
table_comment: Optional[str] = None
fields: Optional[list[FieldPayload]] = PydField(None, description="提供则整体替换;不传则保留旧 fields") fields: Optional[list[FieldPayload]] = PydField(None, description="提供则整体替换;不传则保留旧 fields")
...@@ -121,6 +125,7 @@ class TaskOut(BaseModel): ...@@ -121,6 +125,7 @@ class TaskOut(BaseModel):
rules: int # 规则总数(列表视图用) rules: int # 规则总数(列表视图用)
field_list: list[FieldOut] = [] # 详情用(含 rules);列表视图为空(节省带宽) field_list: list[FieldOut] = [] # 详情用(含 rules);列表视图为空(节省带宽)
checked_at: Optional[str] checked_at: Optional[str]
table_comment: Optional[str] = None # 数据表注释(任务列表 UI 用)
created_at: str created_at: str
updated_at: str updated_at: str
...@@ -270,6 +275,7 @@ def _row_to_out(db: Session, t: Task, *, include_field_list: bool = False) -> Ta ...@@ -270,6 +275,7 @@ def _row_to_out(db: Session, t: Task, *, include_field_list: bool = False) -> Ta
rules=rules_count, rules=rules_count,
field_list=field_list, field_list=field_list,
checked_at=d["checked_at"], checked_at=d["checked_at"],
table_comment=d["table_comment"],
created_at=d["created_at"], created_at=d["created_at"],
updated_at=d["updated_at"], updated_at=d["updated_at"],
) )
...@@ -308,6 +314,8 @@ def create_task(payload: TaskCreate, db: Session = Depends(get_session)) -> Task ...@@ -308,6 +314,8 @@ def create_task(payload: TaskCreate, db: Session = Depends(get_session)) -> Task
description=payload.description, description=payload.description,
db_type=payload.db_type or "MySQL", db_type=payload.db_type or "MySQL",
conn_json=payload.conn_json, conn_json=payload.conn_json,
# 2026-08-21:数据表注释由前端 TaskModal 选表时拉取一起提交,后端不主动查
table_comment=payload.table_comment,
) )
db.add(t) db.add(t)
try: try:
......
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