From ef91e189dde1e808a8f295d6b5967d407aabf686 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Thu, 30 Jul 2026 14:27:25 +0800 Subject: [PATCH] feat(oa): validate CC assigneeTypes on publish Co-authored-by: Cursor --- backend/app/services/oa_publish.py | 37 ++++++++++------ backend/tests/test_oa_publish.py | 69 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/backend/app/services/oa_publish.py b/backend/app/services/oa_publish.py index e3441708..3c9f8ca8 100644 --- a/backend/app/services/oa_publish.py +++ b/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 [] diff --git a/backend/tests/test_oa_publish.py b/backend/tests/test_oa_publish.py index 996b29ae..184c064e 100644 --- a/backend/tests/test_oa_publish.py +++ b/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",