| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- """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 '(空)'}")
|