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: