feat(oa): validate CC assigneeTypes on publish

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-30 14:27:25 +08:00
1 parent 2326f8bf99
commit ef91e189dd
2 files changed
+93 -13

No files matched your search

+24 -13
View File
@@ -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
View File
@@ -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",