fix(feasibility): promote_to_project幂等守卫——E2E实测重复调用重复建项目;description带[report_id=X]精确标记,立项前查已立则返回既有项目;handler区分reused文案;E2E L3改为幂等断言(不重复建+库中仅1项目)

This commit is contained in:
yumoqing 2026-09-14 14:11:10 +08:00
parent 2304b86667
commit 7a8afab054
3 changed files with 28 additions and 10 deletions

View File

@ -600,9 +600,13 @@ async def _h_promote_to_project(sor, p, ctx):
rid = (p.get("report_id") or "").strip()
if not rid:
return _fmt(False, "report_id 必填")
ok, pid = await promote_to_project(sor, rid, ctx)
ok, res = await promote_to_project(sor, rid, ctx)
if not ok:
return _fmt(False, pid)
return _fmt(False, res)
if isinstance(res, dict) and res.get("reused"):
return _fmt(True, "本报告已立项(幂等):project_id=%s 项目「%s」,不重复建"
% (res.get("project_id"), res.get("project_name", "")))
pid = res.get("project_id") if isinstance(res, dict) else res
return _fmt(True, "已立项 project_id=%s(pipeline=opportunity_general,"
"project_type=demand_mining)" % pid)

View File

@ -285,12 +285,21 @@ async def promote_to_project(sor, report_id, ctx):
return False, "报告未过人工确认门禁(status=%s),不能立项" % (rep.get("status") or "draft")
if (rep.get("appr_status") or "") != "approved":
return False, "研发审批未通过(status=%s),不能立项" % (rep.get("appr_status") or "无审批单")
# 幂等守卫(2026-09-14 E2E 实测:重复调用会重复建项目):description 带
# report_id 精确标记,立项前先查——已立过则返回既有项目,不再新建。
desc = "由需求挖掘可行性报告立项:%s [report_id=%s]" % (rep.get("title"), report_id)
exist = await sor.sqlExe(
"SELECT id, name FROM sd_projects WHERE description=${d}$ LIMIT 1", {"d": desc})
await sor.sqlExe("COMMIT", {})
if exist:
return True, {"project_id": getattr(exist[0], "id", ""),
"project_name": getattr(exist[0], "name", ""), "reused": True}
from pipeline_service.project_capability import create_project
ok, pid = await create_project(
name="%s 研发项目" % (rep.get("software") or "未命名"),
project_type="demand_mining", description="由需求挖掘可行性报告立项:%s" % rep.get("title"),
project_type="demand_mining", description=desc,
org_id=ctx.get("org_id") or "0", created_by=ctx.get("user_id") or "",
pipeline_id="opportunity_general")
if not ok:
return False, "立项失败: %s" % pid
return True, pid
return True, {"project_id": pid, "reused": False}

View File

@ -280,13 +280,18 @@ async def m():
np is not None and np.project_type == "demand_mining"
and np.pipeline_id == "opportunity_general" and np.org_id == "0",
"%s/%s/%s" % (getattr(np, "project_type", ""), getattr(np, "pipeline_id", ""), getattr(np, "org_id", "")))
# 重复立项行为记录(当前实现无幂等守卫,重复调用会再建项目——如实记录,清理兜底)
# 幂等守卫:重复 promote 必须复用既有项目,不再新建
l2 = await call("opp_promote_to_project", {"report_id": RID})
DUP = l2.startswith("OK:") and "project_id=" in l2
if DUP:
DUPPID = l2.split("project_id=")[1].split("(")[0].strip()
print("-- FINDING: promote 重复调用再建项目 %s(无幂等守卫)--" % DUPPID, flush=True)
check("L3 重复立项行为已记录", l2.startswith("OK:") or l2.startswith("ERROR:"), l2[:120])
DUPPID = ""
if l2.startswith("OK:") and "project_id=" in l2:
cand = l2.split("project_id=")[1].split("(")[0].split(" ")[0].strip()
if cand and cand != NEWPID:
DUPPID = cand
n_prj = await sql("SELECT COUNT(*) n FROM sd_projects WHERE description LIKE ${p}$",
{"p": "由需求挖掘可行性报告立项%%[report_id=%s]%%" % RID}, one=True)
check("L3 重复立项幂等(不重复建)", l2.startswith("OK:") and "幂等" in l2
and not DUPPID and getattr(n_prj, "n", 99) == 1,
"%s | dup=%s | n=%s" % (l2[:120], DUPPID or "-", getattr(n_prj, "n", "?")))
# ── M. 清理(报告+PPT保留供浏览器验收;立项项目删除)──
PROMOTED = [x for x in (NEWPID, locals().get("DUPPID", "")) if x]