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