From 87ecf3ee72c20a23aef1fb066d60c5d7da2a3d75 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 15 Sep 2026 15:27:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(bidding):=20=E6=A8=A1=E7=89=88=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E9=97=A8=E5=8A=A0=E5=9B=BA(E2E=E5=AE=9E=E6=B5=8B)?= =?UTF-8?q?=E2=80=94=E2=80=94confirm=5Ftech=5Ftemplate=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=94=B9=E5=A4=8D=E7=94=A8=E5=BC=95=E6=93=8E=5Fcheck=5Fconfirm?= =?UTF-8?q?=5Foperator(=E5=90=8C=E6=9C=BA=E6=9E=84+=E8=B6=85=E7=AE=A1/?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E8=80=85,=E4=B8=8Eflow=5Fplan=5Fconfirm?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E4=B8=80=E8=87=B4,=E5=8E=9F=E8=87=AA?= =?UTF-8?q?=E5=86=99'=E4=BB=85=E5=90=8C=E6=9C=BA=E6=9E=84'=E4=B8=A2?= =?UTF-8?q?=E4=BA=86=E8=B6=85=E7=AE=A1=E8=AF=AD=E4=B9=89=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?=E8=AF=AF=E6=8B=92admin);=E7=A1=AE=E8=AE=A4=E9=87=8D=E8=90=BD?= =?UTF-8?q?=E5=AE=88=E5=8D=AB:=E5=B7=B2=E6=9C=89=E9=9D=9Epending=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E6=8B=92=E7=BB=9D=E9=87=8D=E6=96=B0=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?(=E9=98=B2=E6=AF=81=E5=B7=B2=E5=86=99=E5=86=85=E5=AE=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_bidding/bid_tech_capability.py | 40 ++++++++++++++++++------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/pipeline_bidding/bid_tech_capability.py b/pipeline_bidding/bid_tech_capability.py index 45f00a8..ac4e78f 100644 --- a/pipeline_bidding/bid_tech_capability.py +++ b/pipeline_bidding/bid_tech_capability.py @@ -445,15 +445,25 @@ async def confirm_tech_template(human_task_id, decision, comment="", if ht.get("status") != "pending": return False, "待办已处理(当前 %s)" % ht.get("status") project_id = ht.get("project_id") or "" - # 同机构校验 - prec = await sor.sqlExe("SELECT org_id FROM sd_projects WHERE id=${p}$", - {"p": project_id}) - await sor.sqlExe("COMMIT", {}) - proj_org = getattr(prec[0], "org_id", "") if prec else "" - from pipeline_service.human_task_capability import _get_user_org - user_org = await _get_user_org(sor, operator_id) - if proj_org and user_org != proj_org: - return False, "仅同机构用户可确认该项目模版" + # 确认权限:复用引擎 _check_confirm_operator(同机构 + owner.superuser/项目创建者)—— + # 与 flow_plan_confirm 确认门语义完全一致,不自己另写一套校验(2026-09-15 E2E 实测 + # 发现自写「仅同机构」把超管/创建者语义丢了)。 + try: + from pipeline_service.flow_plan_capability import _check_confirm_operator + ok_op, err_op = await _check_confirm_operator( + sor, {"project_id": project_id}, operator_id) + if not ok_op: + return False, err_op or "无权确认该项目模版" + except ImportError: + # 引擎不可用(理论不发生):兜底同机构校验 + prec = await sor.sqlExe("SELECT org_id FROM sd_projects WHERE id=${p}$", + {"p": project_id}) + await sor.sqlExe("COMMIT", {}) + proj_org = getattr(prec[0], "org_id", "") if prec else "" + from pipeline_service.human_task_capability import _get_user_org + user_org = await _get_user_org(sor, operator_id) + if proj_org and user_org != proj_org: + return False, "仅同机构用户可确认该项目模版" if decision == "confirm": schema = json_loads(ht.get("form_schema"), {}) or {} @@ -462,7 +472,17 @@ async def confirm_tech_template(human_task_id, decision, comment="", chapters = schema.get("chapters") or [] if not chapters: return False, "待办里没有骨架数据(form_schema.chapters 空),无法落库" - # 幂等:已有骨架先清(驳回重提后再确认的场景) + # 幂等重落守卫:仅当既有章节全部未开写(pending)时清旧骨架—— + # 已有 writing/written/approved 章节说明流程已推进,重新确认会毁掉 + # 已写内容,拒绝并提示走驳回/人工处置(2026-09-15 加固)。 + busy = await sor.sqlExe( + "SELECT COUNT(*) AS c FROM bid_chapters WHERE project_id=${p}$ " + "AND status<>${s}$", {"p": project_id, "s": CH_PENDING}) + await sor.sqlExe("COMMIT", {}) + if to_int(getattr(busy[0], "c", 0) if busy else 0) > 0: + return False, ("项目已有开写/已完成的章节,不能重新确认骨架(会毁掉已写内容)。" + "如需换骨架请先驳回或在章节管理页人工处置既有章节。") + # 幂等:清掉此前确认落库但尚未开写的旧骨架(驳回重提后再确认的场景) await sor.sqlExe( "DELETE FROM bid_chapters WHERE project_id=${p}$", {"p": project_id}) n = 0