"""OA 审批人解析(不读 sys_role;职位成员走 sys_user_post)""" from __future__ import annotations from typing import Any, Dict, List, Optional, Tuple from sqlalchemy.orm import Session from app.crud import user as user_crud from app.models.models import SysDept, SysUser class AssigneeResolveError(ValueError): """审批人解析失败""" VALID_ASSIGNEE_TYPES = frozenset( { "users", "posts", "dept_leader", "direct_supervisor", "initiator_select", } ) def normalize_cc_assignee_types(assignee_config: Optional[Dict[str, Any]]) -> List[str]: cfg = assignee_config or {} raw = cfg.get("assigneeTypes") or cfg.get("assignee_types") out: List[str] = [] seen = set() if isinstance(raw, list) and raw: for item in raw: t = str(item or "").strip() if t in VALID_ASSIGNEE_TYPES and t not in seen: seen.add(t) out.append(t) return out single = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip() if single in VALID_ASSIGNEE_TYPES: return [single] return [] def resolve_cc_assignees( db: Session, *, tenant_no: str, assignee_config: Dict[str, Any], initiator_user_id: str, initiator_dept_id: Optional[str] = None, advanced: Optional[Dict[str, Any]] = None, initiator_select_user_ids: Optional[List[str]] = None, ) -> Tuple[List[str], Dict[str, Any]]: """抄送多策略软并集:单策略失败/空则跳过;全部无人返回 [],不抛错。""" cfg = assignee_config or {} types = normalize_cc_assignee_types(cfg) merged: List[str] = [] seen = set() sources: List[str] = [] skipped: List[Dict[str, str]] = [] for t in types: single_cfg = {**cfg, "assigneeType": t} try: users, meta = resolve_assignees( db, tenant_no=tenant_no, assignee_config=single_cfg, initiator_user_id=initiator_user_id, initiator_dept_id=initiator_dept_id, advanced=advanced, initiator_select_user_ids=initiator_select_user_ids, ) except AssigneeResolveError as e: skipped.append({"type": t, "reason": str(e)}) continue if not users: skipped.append({"type": t, "reason": "empty"}) continue src = str((meta or {}).get("resolve_source") or t) sources.append(src) for uid in users: if uid and uid not in seen: seen.add(uid) merged.append(uid) if len(sources) == 1: resolve_source = sources[0] elif len(sources) > 1: resolve_source = "multi" else: resolve_source = "" return merged, { "resolve_source": resolve_source, "sources": sources, "skipped": skipped, "assigneeTypes": types, } def _active_user_id(db: Session, user_id: Optional[str], tenant_no: str) -> Optional[str]: uid = (user_id or "").strip() if not uid: return None u = ( db.query(SysUser) .filter( SysUser.id == uid, SysUser.tenant_no == tenant_no, SysUser.del_flag == 0, SysUser.status == "1", ) .first() ) return u.id if u else None def resolve_dept_leader( db: Session, *, tenant_no: str, initiator_dept_id: Optional[str] ) -> Tuple[List[str], Dict[str, Any]]: dept_id = (initiator_dept_id or "").strip() if not dept_id: raise AssigneeResolveError("发起人部门为空,无法解析部门负责人") dept = ( db.query(SysDept) .filter(SysDept.id == dept_id, SysDept.tenant_no == tenant_no, SysDept.del_flag == 0) .first() ) if not dept: raise AssigneeResolveError("发起人部门不存在") leader_uid = _active_user_id(db, getattr(dept, "leader_user_id", None), tenant_no) if not leader_uid: raise AssigneeResolveError("部门负责人未配置或用户无效,请在组织管理中维护负责人") return [leader_uid], {"resolve_source": "dept_leader"} def resolve_direct_supervisor( db: Session, *, tenant_no: str, initiator_user_id: str, initiator_dept_id: Optional[str], fallback_to_dept_leader: bool = True, ) -> Tuple[List[str], Dict[str, Any]]: initiator = ( db.query(SysUser) .filter( SysUser.id == initiator_user_id, SysUser.tenant_no == tenant_no, SysUser.del_flag == 0, ) .first() ) if not initiator: raise AssigneeResolveError("发起人不存在") supervisor_uid = _active_user_id( db, getattr(initiator, "supervisor_user_id", None), tenant_no ) if supervisor_uid: # 允许主管配置为本人(如部门经理),由高级设置 autoPassWhenApproverIsInitiator 决定是否自动通过 return [supervisor_uid], {"resolve_source": "direct_supervisor"} if fallback_to_dept_leader: users, meta = resolve_dept_leader( db, tenant_no=tenant_no, initiator_dept_id=initiator_dept_id or initiator.dept_id ) meta = {**meta, "resolve_source": "supervisor_fallback_dept_leader"} return users, meta raise AssigneeResolveError("未配置直属主管,且未开启回退部门负责人") def resolve_assignees( db: Session, *, tenant_no: str, assignee_config: Dict[str, Any], initiator_user_id: str, initiator_dept_id: Optional[str] = None, advanced: Optional[Dict[str, Any]] = None, initiator_select_user_ids: Optional[List[str]] = None, ) -> Tuple[List[str], Dict[str, Any]]: """ 按节点 approverConfig 解析处理人。 assigneeType: users | posts | dept_leader | direct_supervisor | initiator_select """ cfg = assignee_config or {} assignee_type = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip() advanced = advanced or {} fallback = advanced.get("supervisorFallbackToDeptLeader", True) if isinstance(fallback, str): fallback = fallback.lower() in ("1", "true", "yes") if assignee_type == "users": ids = cfg.get("userIds") or cfg.get("user_ids") or [] out: List[str] = [] seen = set() for uid in ids: real = _active_user_id(db, uid, tenant_no) if real and real not in seen: seen.add(real) out.append(real) if not out: raise AssigneeResolveError("指定成员为空或均无效") return out, {"resolve_source": "users"} if assignee_type == "posts": post_ids = cfg.get("postIds") or cfg.get("post_ids") or [] users = user_crud.list_user_ids_bound_to_posts(db, list(post_ids), tenant_no) out = [] seen = set() for uid in users: real = _active_user_id(db, uid, tenant_no) if real and real not in seen: seen.add(real) out.append(real) if not out: raise AssigneeResolveError("职位成员为空或均无效") return out, {"resolve_source": "posts"} if assignee_type == "dept_leader": return resolve_dept_leader( db, tenant_no=tenant_no, initiator_dept_id=initiator_dept_id ) if assignee_type == "direct_supervisor": return resolve_direct_supervisor( db, tenant_no=tenant_no, initiator_user_id=initiator_user_id, initiator_dept_id=initiator_dept_id, fallback_to_dept_leader=bool(fallback), ) if assignee_type == "initiator_select": ids = initiator_select_user_ids or cfg.get("userIds") or [] out = [] seen = set() for uid in ids: real = _active_user_id(db, uid, tenant_no) if real and real not in seen: seen.add(real) out.append(real) if not out: raise AssigneeResolveError("发起人未选择审批人") return out, {"resolve_source": "initiator_select"} raise AssigneeResolveError(f"不支持的审批人类型: {assignee_type or '(空)'}")