From cdc87f6a6d8718b5ff40ab803206e8478d5811fe Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 18 Sep 2026 17:17:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(qc-race):=20=E6=9C=AB=E8=BD=AEQC=E5=9C=A8?= =?UTF-8?q?=E5=8A=9E=E6=9C=9F=E9=97=B4=E8=AF=AF=E5=88=A4=E8=BD=AE=E6=AC=A1?= =?UTF-8?q?=E7=94=A8=E5=B0=BD+=E7=BA=AF=E6=8A=80=E6=9C=AF=E6=96=B9?= =?UTF-8?q?=E6=A1=88=E5=88=86=E6=94=AF=E7=BC=BA=E5=8D=87=E7=BA=A7=E5=BE=85?= =?UTF-8?q?=E5=8A=9E=E8=87=AA=E5=8A=A8=E5=85=B3=E9=97=AD(2026-09-18?= =?UTF-8?q?=E9=93=B6=E8=81=94=E5=AE=9E=E9=94=A4)=E2=80=94=E2=80=94?= =?UTF-8?q?=E2=91=A0=E5=85=B1=E7=94=A8=E6=AE=B5/=E6=A0=87=E5=87=86A2:?= =?UTF-8?q?=E5=9C=A8=E5=8A=9E=E5=AE=88=E5=8D=AB=E5=85=88=E4=BA=8E=E8=BD=AE?= =?UTF-8?q?=E6=AC=A1=E7=94=A8=E5=B0=BD=E5=88=A4=E5=AE=9A(start=5Fqc?= =?UTF-8?q?=E5=BC=80=E8=BD=AE=E5=8D=B3=E5=BB=BApassed=3D0=E8=AE=B0?= =?UTF-8?q?=E5=BD=95,=E8=AF=84=E5=88=86=E5=AE=8C=E6=88=90=E6=89=8DUPDATE;?= =?UTF-8?q?=E6=97=A7=E9=A1=BA=E5=BA=8F=E5=9C=A8=E6=9C=AB=E8=BD=AE=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E5=9C=A8=E5=8A=9E=E6=97=B6=E8=AF=AF=E5=8F=91qc=5Fesca?= =?UTF-8?q?lation=E5=BE=85=E5=8A=9E,=E9=93=B6=E8=81=9416:21:40=E8=AF=AF?= =?UTF-8?q?=E5=8F=91=E5=AE=9E=E5=BD=95,16:24:20=E7=AC=AC3=E8=BD=AE8.60?= =?UTF-8?q?=E5=88=86=E9=80=9A=E8=BF=87);A2=E8=BF=87=E6=BB=A4=E5=90=8C?= =?UTF-8?q?=E5=8A=A0=5Fopen=5Fqc=5Ftype=5Ftasks=E5=9C=A8=E5=8A=9E=E8=B1=81?= =?UTF-8?q?=E5=85=8D;=E2=91=A1=E7=BA=AF=E6=8A=80=E6=9C=AF=E6=96=B9?= =?UTF-8?q?=E6=A1=88=E5=88=86=E6=94=AFT2b:=E8=A1=A5qc=5Fescalation?= =?UTF-8?q?=E5=BE=85=E5=8A=9E=E8=87=AA=E5=8A=A8=E5=85=B3=E9=97=AD=E6=AE=B5?= =?UTF-8?q?(=E6=A0=87=E5=87=86=E6=B5=81=E7=A8=8B=E6=9C=89A2b,tech=5Fpropos?= =?UTF-8?q?al=E5=88=86=E6=94=AFreconcile=E6=8F=90=E5=89=8Dreturn=E4=B8=8D?= =?UTF-8?q?=E7=BB=8F=E8=BF=87=E2=86=92=E8=AF=AF=E5=8F=91/=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E5=8D=87=E7=BA=A7=E5=BE=85=E5=8A=9E=E6=B0=B8=E4=B9=85?= =?UTF-8?q?=E6=8C=82=E8=A7=92=E6=A0=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_bidding/bid_flow.py | 42 ++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/pipeline_bidding/bid_flow.py b/pipeline_bidding/bid_flow.py index 4b37ffa..df38cc6 100644 --- a/pipeline_bidding/bid_flow.py +++ b/pipeline_bidding/bid_flow.py @@ -913,6 +913,12 @@ async def reconcile_project(sor, project_id, project_name=""): if qc_pending: _kept = [] for _qp in qc_pending: + # 审核任务在办 → 最新一轮记录尚未落定(start_qc 开轮即建 passed=0 记录, + # 评分在完成时才 UPDATE),不得参与「轮次用尽」判定或派发,一律等待 + # (2026-09-18 银联末轮误报修复的标准流程同款守卫)。 + if await _open_qc_type_tasks(sor, project_id, _qp[0]) > 0: + acts.append("waiting: 类型「%s」审核任务在办,最新一轮尚未落定" % _qp[0]) + continue _d = QC_TYPE_TO_DIM.get(_qp[0], "") if _d and await _open_dim_tasks(sor, project_id, _d) > 0: acts.append("waiting: 维度「%s」分析任务在办,%s 暂缓 QC 审核(防审半成品)" @@ -1598,6 +1604,19 @@ async def _reconcile_tech_analysis_qc(sor, project_id, pname, n_items, latest_qc if latest_qc and str(latest_qc.get("passed")) == "1": return True, False, False + # 在办守卫必须先于「轮次用尽」判定(2026-09-18 银联末轮误报修复): + # start_qc 在末轮审核开轮时即建记录(passed=0、未评分),此刻任务仍在办; + # 旧顺序会先判 rnd>=qc_max_round 命中「轮次用尽」误发 qc_escalation 待办, + # 而实际末轮审核正在跑、随时可能评分通过。QC 任务在办 → 最新轮尚未落定,一律等待。 + if await _open_tech_tasks(sor, project_id, "bid_qc") > 0: + acts.append("waiting: 技术评估项 QC 任务在办") + return False, False, True + # 竞态守卫:抽取/重做任务在办 → 不派审核(防对半成品打分), + # 同样不得判「轮次用尽」(重做正在跑,不是最终失败)。 + if await _open_tech_tasks(sor, project_id, "bid_tech_analysis") > 0: + acts.append("waiting: 技术评估项抽取任务在办,暂不派 tech QC") + return False, False, True + rnd = to_int(latest_qc.get("round"), 0) if latest_qc else 0 if latest_qc and rnd >= th["qc_max_round"]: # 轮次用尽:产出未更新则不空转,抛人工(同标准流程逃逸阀) @@ -1614,13 +1633,6 @@ async def _reconcile_tech_analysis_qc(sor, project_id, pname, n_items, latest_qc await sor.sqlExe("COMMIT", {}) acts.append("escalated: 技术评估项 QC 轮次用尽 → 人工介入") return False, True, True - if await _open_tech_tasks(sor, project_id, "bid_qc") > 0: - acts.append("waiting: 技术评估项 QC 任务在办") - return False, False, True - # 竞态守卫:抽取/重做任务在办 → 不派审核(防对半成品打分) - if await _open_tech_tasks(sor, project_id, "bid_tech_analysis") > 0: - acts.append("waiting: 技术评估项抽取任务在办,暂不派 tech QC") - return False, False, True tid = await create_role_task( sor, project_id, R_QC, "%s 技术评估项契合度审核(对标招标技术需求原文)" % pname, @@ -1686,6 +1698,22 @@ async def _reconcile_tech_proposal(sor, project_id, pname, stage_table): qc_passed, _tech_over, _blocked = await _reconcile_tech_analysis_qc( sor, project_id, pname, n_items, latest_qc, th, _on, FLOW_KEY_TECH_PROPOSAL, acts) + + # ── T2b. qc_escalation 待办自动关闭(2026-09-18 银联实锤缺口)── + # 标准流程有 A2b 段,纯技术方案分支在 reconcile_project 提前 return 不经过它 → + # tech_items 轮次用尽发的升级待办在问题解决后(QC 通过/人工放行/重做恢复) + # 永远挂着,角标常驻误导。逃逸阀闭环的另一半:问题解决 → 待办自动收。 + if not _tech_over: + _ht_esc = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending") + if _ht_esc: + await sor.sqlExe( + "UPDATE pipeline_human_tasks SET status='done', qc_status='passed', " + "qc_comment='对账器自动关闭:技术评估项 QC 已通过/放行或流程已恢复', " + "submitted_at=NOW() WHERE id=${h}$ AND status='pending'", + {"h": _ht_esc.get("id")}) + await sor.sqlExe("COMMIT", {}) + acts.append("auto-closed: qc_escalation 待办(tech_items 已通过/流程已恢复)") + if not qc_passed: return acts or ["idle: 技术需求解析/审核中"] if _blocked: