From 3d88f52e2e47d0ea57fdcf7695277d4dec601086 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Thu, 30 Jul 2026 13:04:24 +0800 Subject: [PATCH] feat(backend): match __initiator__ contains in condition groups Co-authored-by: Cursor --- backend/app/services/oa_engine.py | 51 +++++++++++++++++++++++++------ backend/tests/test_oa_engine.py | 45 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/backend/app/services/oa_engine.py b/backend/app/services/oa_engine.py index 47c8912c..fc854556 100644 --- a/backend/app/services/oa_engine.py +++ b/backend/app/services/oa_engine.py @@ -32,6 +32,8 @@ class OaEngineError(ValueError): _AUTO_PASS_COMMENT = "系统自动通过" +INITIATOR_CONDITION_FIELD = "__initiator__" + def _loads(text: Optional[str], default: Any = None) -> Any: if text is None or text == "": @@ -351,7 +353,10 @@ def _pending_people_for_node( def _iter_resolved_path_nodes( - root: Optional[Dict[str, Any]], form_data: Optional[Dict[str, Any]] + root: Optional[Dict[str, Any]], + form_data: Optional[Dict[str, Any]], + *, + initiator_user_id: Optional[str] = None, ) -> Optional[List[Dict[str, Any]]]: """沿条件命中路径收集节点;无法匹配时返回 None。""" if not isinstance(root, dict): @@ -365,12 +370,16 @@ def _iter_resolved_path_nodes( out.append(cur) break if ntype == "condition": - g = _match_condition_group(cur.get("conditionGroups") or [], data) + g = _match_condition_group( + cur.get("conditionGroups") or [], data, initiator_user_id=initiator_user_id + ) if not g: return None child = g.get("child") if isinstance(child, dict): - sub = _iter_resolved_path_nodes(child, data) + sub = _iter_resolved_path_nodes( + child, data, initiator_user_id=initiator_user_id + ) if sub is None: return None out.extend(sub) @@ -396,7 +405,9 @@ def _append_pending_nodes_on_path( initiator_select_by_node: Optional[Dict[str, List[str]]], ) -> None: """按命中路径列出全部后续审批/抄送/结束节点(含人员),对齐发起预览。""" - path = _iter_resolved_path_nodes(flow_root, form_data) + path = _iter_resolved_path_nodes( + flow_root, form_data, initiator_user_id=initiator_user_id + ) if path is None: already = { (x.get("nodeKey") or "").strip() @@ -1180,7 +1191,9 @@ def _walk_preview_nodes( if ntype == "condition": groups = cur.get("conditionGroups") or [] - g = _match_condition_group(groups, data) + g = _match_condition_group( + groups, data, initiator_user_id=initiator_user_id + ) if not g: return False child = g.get("child") @@ -1398,7 +1411,12 @@ def _create_tasks_for_node( return created -def _eval_condition_rules(rules: List[Any], form_data: Dict[str, Any]) -> bool: +def _eval_condition_rules( + rules: List[Any], + form_data: Dict[str, Any], + *, + initiator_user_id: Optional[str] = None, +) -> bool: """AND 多条 {field, op, value};contains 走人员集合交集,其余走公式。""" from app.services.oa_person_select import person_contains_match @@ -1411,6 +1429,12 @@ def _eval_condition_rules(rules: List[Any], form_data: Dict[str, Any]) -> bool: if field is None: return False if op == "contains": + if field == INITIATOR_CONDITION_FIELD: + uid = (initiator_user_id or "").strip() + allowed = {str(x or "").strip() for x in (val or [])} + if not uid or uid not in allowed: + return False + continue if not person_contains_match(form_data.get(field), val): return False continue @@ -1426,7 +1450,12 @@ def _eval_condition_rules(rules: List[Any], form_data: Dict[str, Any]) -> bool: return True -def _match_condition_group(groups: List[Any], form_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _match_condition_group( + groups: List[Any], + form_data: Dict[str, Any], + *, + initiator_user_id: Optional[str] = None, +) -> Optional[Dict[str, Any]]: default_g = None for g in groups: if not isinstance(g, dict): @@ -1453,7 +1482,9 @@ def _match_condition_group(groups: List[Any], form_data: Dict[str, Any]) -> Opti continue if isinstance(rules, list) and rules: try: - if _eval_condition_rules(rules, form_data): + if _eval_condition_rules( + rules, form_data, initiator_user_id=initiator_user_id + ): return g except FormulaError: continue @@ -1479,7 +1510,9 @@ def advance_from( ntype = _node_type(node) if ntype == "condition": groups = node.get("conditionGroups") or [] - g = _match_condition_group(groups, form_data) + g = _match_condition_group( + groups, form_data, initiator_user_id=inst.initiator_user_id + ) if not g or not isinstance(g.get("child"), dict): raise OaEngineError("条件分支无法匹配出线") return g["child"] diff --git a/backend/tests/test_oa_engine.py b/backend/tests/test_oa_engine.py index fedc700a..cd753167 100644 --- a/backend/tests/test_oa_engine.py +++ b/backend/tests/test_oa_engine.py @@ -112,6 +112,51 @@ class TestOaEngineHelpers(unittest.TestCase): ) self.assertEqual(g["name"], "hit") + def test_condition_contains_initiator(self): + groups = [ + { + "name": "hit", + "rules": [ + { + "field": "__initiator__", + "op": "contains", + "value": ["u1", "u2"], + } + ], + "child": {"id": "n1", "type": "end"}, + }, + {"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}}, + ] + g = _match_condition_group(groups, {}, initiator_user_id="u2") + self.assertEqual(g["name"], "hit") + g2 = _match_condition_group(groups, {}, initiator_user_id="u9") + self.assertEqual(g2["name"], "default") + + def test_condition_initiator_and_form_field(self): + groups = [ + { + "name": "both", + "rules": [ + {"field": "__initiator__", "op": "contains", "value": ["u1"]}, + {"field": "days", "op": ">", "value": 3}, + ], + "child": {"id": "n1", "type": "end"}, + }, + {"name": "default", "isDefault": True, "child": {"id": "n2", "type": "end"}}, + ] + self.assertEqual( + _match_condition_group( + groups, {"days": 5}, initiator_user_id="u1" + )["name"], + "both", + ) + self.assertEqual( + _match_condition_group( + groups, {"days": 5}, initiator_user_id="u9" + )["name"], + "default", + ) + def test_flow_node_key_order_cc_before_approval(self): root = { "id": "s",