fix(delete_project)+feat(cleanup_orphans): 删除项目补 task_id 级联(sd_deliverables/artifacts/task_steps/human_tasks)+sd_project_role_models;新增孤儿清扫能力(指向已删项目/迭代/任务的残留,dry_run可先统计)
This commit is contained in:
parent
ccba6f470e
commit
8eeb196515
@ -412,6 +412,26 @@ async def delete_project(project_id, who=None, agent_id=None, confirm=False):
|
||||
for sql in indirect:
|
||||
await sor.sqlExe(sql, {"pid": project_id})
|
||||
|
||||
# 2b. 任务关联表:先取本项目的任务 id,再删 task_id 指向它们的行
|
||||
# (pipeline_deliverables 等同时有 project_id 和 task_id 两列,仅按 project_id
|
||||
# 删会漏掉「项目删除时 task 已先被删、project_id 已失效」的残留)
|
||||
tid_rows = await sor.sqlExe(
|
||||
"SELECT id FROM pipeline_tasks WHERE tenant_id=${pid}$", {"pid": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
task_ids = [getattr(r, 'id', '') for r in (tid_rows or []) if getattr(r, 'id', '')]
|
||||
if task_ids:
|
||||
tid_list = ",".join("'" + t.replace("'", "") + "'" for t in task_ids)
|
||||
for sql in [
|
||||
"DELETE FROM pipeline_deliverables WHERE task_id COLLATE utf8mb4_unicode_ci IN (" + tid_list + ")",
|
||||
"DELETE FROM pipeline_artifacts WHERE task_id IN (" + tid_list + ")",
|
||||
"DELETE FROM pipeline_task_steps WHERE task_id IN (" + tid_list + ")",
|
||||
"DELETE FROM pipeline_human_tasks WHERE task_id IN (" + tid_list + ")",
|
||||
]:
|
||||
try:
|
||||
await sor.sqlExe(sql, {})
|
||||
except Exception as e:
|
||||
logger.warning("delete_project task-linked cleanup failed: %s err=%s", sql, e)
|
||||
|
||||
# 3. 删除直接关联表(project_id / tenant_id)
|
||||
direct = [
|
||||
"DELETE FROM pipeline_deliverables WHERE project_id COLLATE utf8mb4_unicode_ci=${pid}$",
|
||||
@ -420,6 +440,7 @@ async def delete_project(project_id, who=None, agent_id=None, confirm=False):
|
||||
"DELETE FROM sd_deploy_envs WHERE project_id=${pid}$",
|
||||
"DELETE FROM sd_features WHERE project_id=${pid}$",
|
||||
"DELETE FROM sd_project_repos WHERE project_id COLLATE utf8mb4_unicode_ci=${pid}$",
|
||||
"DELETE FROM sd_project_role_models WHERE project_id=${pid}$",
|
||||
"DELETE FROM pipeline_tasks WHERE tenant_id=${pid}$",
|
||||
"DELETE FROM audit_log WHERE tenant_id COLLATE utf8mb4_unicode_ci=${pid}$",
|
||||
"DELETE FROM sd_iterations WHERE project_id=${pid}$",
|
||||
@ -438,3 +459,113 @@ async def delete_project(project_id, who=None, agent_id=None, confirm=False):
|
||||
|
||||
logger.info("delete_project: %s done, backup=%s", project_id, backup_msg)
|
||||
return True, f"项目已删除,归档备份在 {backup_msg}"
|
||||
|
||||
|
||||
# ── 孤儿清扫 ────────────────────────────────────────────────────────
|
||||
#
|
||||
# 历史缺陷与手工测试会留下「指向已删项目/迭代/任务的孤儿行」。本函数逐表清扫:
|
||||
# 孤儿判据 = 外键列非空,但其指向的主键在父表中已不存在。全部幂等,可反复执行。
|
||||
#
|
||||
# 列 collation 不一(sd_projects.id 是 unicode_ci,sd_project_repos/pipeline_deliverables/
|
||||
# audit_log 的引用列是 general_ci),跨表比较处显式 COLLATE utf8mb4_unicode_ci 规避 1267。
|
||||
|
||||
_ORPHAN_CLEANUPS = [
|
||||
# (label, 计数SQL, 删除SQL) — 计数用 SELECT COUNT(*),删除用对应 DELETE
|
||||
("sd_project_repos(无项目)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_project_repos WHERE project_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_projects)",
|
||||
"DELETE FROM sd_project_repos WHERE project_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_projects)"),
|
||||
("sd_features(无项目)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_features WHERE project_id NOT IN (SELECT id FROM sd_projects)",
|
||||
"DELETE FROM sd_features WHERE project_id NOT IN (SELECT id FROM sd_projects)"),
|
||||
("sd_project_role_models(无项目)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_project_role_models WHERE project_id NOT IN (SELECT id FROM sd_projects)",
|
||||
"DELETE FROM sd_project_role_models WHERE project_id NOT IN (SELECT id FROM sd_projects)"),
|
||||
("pipeline_deliverables(无项目)",
|
||||
"SELECT COUNT(*) AS rcnt FROM pipeline_deliverables WHERE project_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_projects)",
|
||||
"DELETE FROM pipeline_deliverables WHERE project_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_projects)"),
|
||||
("pipeline_deliverables(无任务)",
|
||||
"SELECT COUNT(*) AS rcnt FROM pipeline_deliverables WHERE task_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM pipeline_tasks)",
|
||||
"DELETE FROM pipeline_deliverables WHERE task_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM pipeline_tasks)"),
|
||||
("pipeline_artifacts(无任务)",
|
||||
"SELECT COUNT(*) AS rcnt FROM pipeline_artifacts WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)",
|
||||
"DELETE FROM pipeline_artifacts WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)"),
|
||||
("pipeline_task_steps(无任务)",
|
||||
"SELECT COUNT(*) AS rcnt FROM pipeline_task_steps WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)",
|
||||
"DELETE FROM pipeline_task_steps WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)"),
|
||||
("pipeline_human_tasks(无任务)",
|
||||
"SELECT COUNT(*) AS rcnt FROM pipeline_human_tasks WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)",
|
||||
"DELETE FROM pipeline_human_tasks WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)"),
|
||||
("pipeline_agent_questions(无任务)",
|
||||
"SELECT COUNT(*) AS rcnt FROM pipeline_agent_questions WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)",
|
||||
"DELETE FROM pipeline_agent_questions WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)"),
|
||||
("sd_features(无迭代)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_features WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)",
|
||||
"DELETE FROM sd_features WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)"),
|
||||
("sd_bugs(无迭代)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_bugs WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)",
|
||||
"DELETE FROM sd_bugs WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)"),
|
||||
("sd_test_plans(无迭代)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_test_plans WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)",
|
||||
"DELETE FROM sd_test_plans WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)"),
|
||||
("sd_test_cases(无计划)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_test_cases WHERE plan_id NOT IN (SELECT id FROM sd_test_plans)",
|
||||
"DELETE FROM sd_test_cases WHERE plan_id NOT IN (SELECT id FROM sd_test_plans)"),
|
||||
("sd_conversations(无迭代)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_conversations WHERE iteration_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_iterations)",
|
||||
"DELETE FROM sd_conversations WHERE iteration_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_iterations)"),
|
||||
("sd_iterations(无项目)",
|
||||
"SELECT COUNT(*) AS rcnt FROM sd_iterations WHERE project_id NOT IN (SELECT id FROM sd_projects)",
|
||||
"DELETE FROM sd_iterations WHERE project_id NOT IN (SELECT id FROM sd_projects)"),
|
||||
]
|
||||
|
||||
|
||||
def _count_rows(recs):
|
||||
"""从 SELECT COUNT(*) AS rcnt 结果取整数;兼容 DictObject/dict/list。"""
|
||||
if not recs:
|
||||
return 0
|
||||
r = recs[0]
|
||||
if isinstance(r, dict):
|
||||
v = r.get('rcnt') or r.get('count') or r.get('COUNT(*)')
|
||||
return int(v or 0)
|
||||
if isinstance(r, (list, tuple)):
|
||||
return int(r[0] if r and r[0] is not None else 0)
|
||||
v = getattr(r, 'rcnt', None)
|
||||
if v is None:
|
||||
v = getattr(r, 'count', None)
|
||||
return int(v or 0)
|
||||
|
||||
|
||||
async def cleanup_orphans(confirm=False, dry_run=False):
|
||||
"""清扫指向已删项目/迭代/任务的孤儿行(幂等,可反复执行)。
|
||||
|
||||
confirm 必须为 True。dry_run=True 时只统计不删除。
|
||||
逐表执行,单表失败不中断其余表。
|
||||
返回 (ok, message),message 含每表行数汇总。
|
||||
"""
|
||||
if not confirm:
|
||||
return False, "孤儿清扫需二次确认(confirm=true)"
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
results = []
|
||||
total = 0
|
||||
for label, cnt_sql, del_sql in _ORPHAN_CLEANUPS:
|
||||
try:
|
||||
recs = await sor.sqlExe(cnt_sql, {})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
n = _count_rows(recs)
|
||||
except Exception as e:
|
||||
results.append(f"{label}: 跳过({type(e).__name__})")
|
||||
continue
|
||||
if n and not dry_run:
|
||||
try:
|
||||
await sor.sqlExe(del_sql, {})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
except Exception as e:
|
||||
results.append(f"{label}: 删除失败({type(e).__name__})")
|
||||
continue
|
||||
total += n
|
||||
results.append(f"{label}: {n}")
|
||||
verb = "发现" if dry_run else "清除"
|
||||
logger.info("cleanup_orphans(%s): %s %d 行孤儿记录。明细: %s",
|
||||
"dry_run" if dry_run else "执行", verb, total, "; ".join(results))
|
||||
return True, f"孤儿清扫完成,{verb} {total} 行。明细:{';'.join(results)}"
|
||||
|
||||
@ -188,6 +188,7 @@ SDL_TOOLS = [
|
||||
ToolDefinition(name="resume_project", description="恢复项目推进(paused→active)", parameters={"project_id": "项目ID"}, category="project"),
|
||||
ToolDefinition(name="backup_project", description="归档备份项目(打包工作目录+数据记录为tgz到机构工作目录_archive/)", parameters={"project_id": "项目ID"}, category="project"),
|
||||
ToolDefinition(name="delete_project", description="删除项目(需二次确认confirm=true,删除前自动归档备份tgz)", parameters={"project_id": "项目ID", "confirm": "二次确认,必须为true"}, category="project"),
|
||||
ToolDefinition(name="cleanup_orphans", description="清扫指向已删项目/迭代/任务的孤儿残留记录(需confirm=true,dry_run=true只统计不删)", parameters={"confirm": "二次确认,必须为true", "dry_run": "true=只统计不删除(可选)"}, category="admin"),
|
||||
# ── 迭代 ──
|
||||
ToolDefinition(name="list_iterations", description="查看当前项目迭代列表", parameters={"status": "按状态筛选(可选)"}, category="iteration"),
|
||||
ToolDefinition(name="create_iteration", description="创建迭代(planning)", parameters={"iteration_name": "迭代名称", "iteration_type": "迭代类型(可选)", "scope": "迭代范围(可选)"}, category="iteration"),
|
||||
@ -1226,6 +1227,14 @@ async def _h_delete_project(sor, p, ctx):
|
||||
return f"OK: {msg}" if ok else f"ERROR: {msg}"
|
||||
|
||||
|
||||
async def _h_cleanup_orphans(sor, p, ctx):
|
||||
confirm = (p.get("confirm", "") or "").strip().lower()
|
||||
dry_run = (p.get("dry_run", "") or "").strip().lower() == "true"
|
||||
from .project_capability import cleanup_orphans
|
||||
ok, msg = await cleanup_orphans(confirm=(confirm == "true"), dry_run=dry_run)
|
||||
return f"OK: {msg}" if ok else f"ERROR: {msg}"
|
||||
|
||||
|
||||
# ── 迭代 handler ──
|
||||
|
||||
async def _h_list_iterations(sor, p, ctx):
|
||||
@ -1755,6 +1764,7 @@ SDL_HANDLERS = {
|
||||
"resume_project": _h_resume_project,
|
||||
"backup_project": _h_backup_project,
|
||||
"delete_project": _h_delete_project,
|
||||
"cleanup_orphans": _h_cleanup_orphans,
|
||||
# ── 迭代 ──
|
||||
"list_iterations": _h_list_iterations,
|
||||
"create_iteration": _h_create_iteration,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user