Przeglądaj źródła

feat(oa): validate CC assigneeTypes on publish

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 miesięcy temu
rodzic
commit
ef91e189dd
2 zmienionych plików z 93 dodań i 13 usunięć
  1. 24 13
      backend/app/services/oa_publish.py
  2. 69 0
      backend/tests/test_oa_publish.py

+ 24 - 13
backend/app/services/oa_publish.py

@@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
 
 from app.crud import oa_approval as oa_crud
 from app.models.models import OaRole
+from app.services.oa_assignee import VALID_ASSIGNEE_TYPES, normalize_cc_assignee_types
 
 # 可填控件(FormCreate / 自建控件字典交集;布局类不计)
 FILLABLE_TYPES = frozenset(
@@ -57,16 +58,6 @@ LAYOUT_TYPES = frozenset(
     }
 )
 
-VALID_ASSIGNEE_TYPES = frozenset(
-    {
-        "users",
-        "oa_roles",
-        "dept_leader",
-        "direct_supervisor",
-        "initiator_select",
-    }
-)
-
 VALID_NODE_TYPES = frozenset({"start", "approval", "cc", "condition", "end"})
 
 # 系统条件伪字段(非表单控件)
@@ -212,11 +203,11 @@ def validate_flow_tree(flow_tree: Any) -> List[str]:
                 errors.append(f"{path}:结束节点后不应再有子节点")
             return
 
-        if ntype in ("approval", "cc"):
+        if ntype == "approval":
             cfg = node.get("approverConfig") or node.get("approver_config") or {}
             if not isinstance(cfg, dict):
                 cfg = {}
-            # 抄送也需要明确接收人策略
+            # 审批只认单字段 assigneeType(忽略 assigneeTypes)
             at = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip()
             if not at:
                 errors.append(f"{path}:未配置审批人/抄送人策略")
@@ -231,7 +222,27 @@ def validate_flow_tree(flow_tree: Any) -> List[str]:
                 codes = cfg.get("oaRoleCodes") or cfg.get("oa_role_codes") or []
                 if not rids and not codes:
                     errors.append(f"{path}:工作流角色不能为空")
-            # dept_leader / direct_supervisor / initiator_select:类型本身即完备
+
+        if ntype == "cc":
+            cfg = node.get("approverConfig") or node.get("approver_config") or {}
+            if not isinstance(cfg, dict):
+                cfg = {}
+            types = normalize_cc_assignee_types(cfg)
+            if not types:
+                errors.append(f"{path}:未配置审批人/抄送人策略")
+            else:
+                for at in types:
+                    if at not in VALID_ASSIGNEE_TYPES:
+                        errors.append(f"{path}:不支持的审批人类型「{at}」")
+                    elif at == "users":
+                        ids = cfg.get("userIds") or cfg.get("user_ids") or []
+                        if not ids:
+                            errors.append(f"{path}:指定成员不能为空")
+                    elif at == "oa_roles":
+                        rids = cfg.get("oaRoleIds") or cfg.get("oa_role_ids") or []
+                        codes = cfg.get("oaRoleCodes") or cfg.get("oa_role_codes") or []
+                        if not rids and not codes:
+                            errors.append(f"{path}:工作流角色不能为空")
 
         if ntype == "condition":
             groups = node.get("conditionGroups") or node.get("condition_groups") or []

+ 69 - 0
backend/tests/test_oa_publish.py

@@ -108,6 +108,75 @@ class TestOaPublishValidate(unittest.TestCase):
         errs = validate_flow_tree(tree)
         self.assertTrue(any("指定成员" in e for e in errs))
 
+    def test_cc_multi_types_ok(self):
+        flow = {
+            "type": "start",
+            "child": {
+                "type": "cc",
+                "name": "抄送",
+                "approverConfig": {
+                    "assigneeTypes": ["users", "dept_leader"],
+                    "userIds": ["u1"],
+                },
+                "child": {"type": "end"},
+            },
+        }
+        self.assertEqual(validate_flow_tree(flow), [])
+
+    def test_cc_multi_users_empty_fails(self):
+        flow = {
+            "type": "start",
+            "child": {
+                "type": "cc",
+                "name": "抄送",
+                "approverConfig": {
+                    "assigneeTypes": ["users", "initiator_select"],
+                    "userIds": [],
+                },
+                "child": {"type": "end"},
+            },
+        }
+        errs = validate_flow_tree(flow)
+        self.assertTrue(any("指定成员" in e for e in errs))
+
+    def test_cc_no_types_fails(self):
+        flow = {
+            "type": "start",
+            "child": {
+                "type": "cc",
+                "name": "抄送",
+                "approverConfig": {},
+                "child": {"type": "end"},
+            },
+        }
+        errs = validate_flow_tree(flow)
+        self.assertTrue(len(errs) >= 1)
+
+    def test_approval_ignores_assignee_types_array(self):
+        flow = {
+            "type": "start",
+            "child": {
+                "type": "approval",
+                "name": "审批",
+                "approverConfig": {"assigneeTypes": ["dept_leader"]},
+                "child": {"type": "end"},
+            },
+        }
+        errs = validate_flow_tree(flow)
+        self.assertTrue(len(errs) >= 1)
+
+    def test_cc_legacy_single_assignee_type_ok(self):
+        flow = {
+            "type": "start",
+            "child": {
+                "type": "cc",
+                "name": "抄送",
+                "approverConfig": {"assigneeType": "initiator_select"},
+                "child": {"type": "end"},
+            },
+        }
+        self.assertEqual(validate_flow_tree(flow), [])
+
     def test_condition_branch_incomplete(self):
         tree = {
             "type": "start",