oa_assignee.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. """OA 审批人解析(不读 sys_role;职位成员走 sys_user_post)"""
  2. from __future__ import annotations
  3. from typing import Any, Dict, List, Optional, Tuple
  4. from sqlalchemy.orm import Session
  5. from app.crud import user as user_crud
  6. from app.models.models import SysDept, SysUser
  7. class AssigneeResolveError(ValueError):
  8. """审批人解析失败"""
  9. VALID_ASSIGNEE_TYPES = frozenset(
  10. {
  11. "users",
  12. "posts",
  13. "dept_leader",
  14. "direct_supervisor",
  15. "initiator_select",
  16. }
  17. )
  18. def normalize_cc_assignee_types(assignee_config: Optional[Dict[str, Any]]) -> List[str]:
  19. cfg = assignee_config or {}
  20. raw = cfg.get("assigneeTypes") or cfg.get("assignee_types")
  21. out: List[str] = []
  22. seen = set()
  23. if isinstance(raw, list) and raw:
  24. for item in raw:
  25. t = str(item or "").strip()
  26. if t in VALID_ASSIGNEE_TYPES and t not in seen:
  27. seen.add(t)
  28. out.append(t)
  29. return out
  30. single = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip()
  31. if single in VALID_ASSIGNEE_TYPES:
  32. return [single]
  33. return []
  34. def resolve_cc_assignees(
  35. db: Session,
  36. *,
  37. tenant_no: str,
  38. assignee_config: Dict[str, Any],
  39. initiator_user_id: str,
  40. initiator_dept_id: Optional[str] = None,
  41. advanced: Optional[Dict[str, Any]] = None,
  42. initiator_select_user_ids: Optional[List[str]] = None,
  43. ) -> Tuple[List[str], Dict[str, Any]]:
  44. """抄送多策略软并集:单策略失败/空则跳过;全部无人返回 [],不抛错。"""
  45. cfg = assignee_config or {}
  46. types = normalize_cc_assignee_types(cfg)
  47. merged: List[str] = []
  48. seen = set()
  49. sources: List[str] = []
  50. skipped: List[Dict[str, str]] = []
  51. for t in types:
  52. single_cfg = {**cfg, "assigneeType": t}
  53. try:
  54. users, meta = resolve_assignees(
  55. db,
  56. tenant_no=tenant_no,
  57. assignee_config=single_cfg,
  58. initiator_user_id=initiator_user_id,
  59. initiator_dept_id=initiator_dept_id,
  60. advanced=advanced,
  61. initiator_select_user_ids=initiator_select_user_ids,
  62. )
  63. except AssigneeResolveError as e:
  64. skipped.append({"type": t, "reason": str(e)})
  65. continue
  66. if not users:
  67. skipped.append({"type": t, "reason": "empty"})
  68. continue
  69. src = str((meta or {}).get("resolve_source") or t)
  70. sources.append(src)
  71. for uid in users:
  72. if uid and uid not in seen:
  73. seen.add(uid)
  74. merged.append(uid)
  75. if len(sources) == 1:
  76. resolve_source = sources[0]
  77. elif len(sources) > 1:
  78. resolve_source = "multi"
  79. else:
  80. resolve_source = ""
  81. return merged, {
  82. "resolve_source": resolve_source,
  83. "sources": sources,
  84. "skipped": skipped,
  85. "assigneeTypes": types,
  86. }
  87. def _active_user_id(db: Session, user_id: Optional[str], tenant_no: str) -> Optional[str]:
  88. uid = (user_id or "").strip()
  89. if not uid:
  90. return None
  91. u = (
  92. db.query(SysUser)
  93. .filter(
  94. SysUser.id == uid,
  95. SysUser.tenant_no == tenant_no,
  96. SysUser.del_flag == 0,
  97. SysUser.status == "1",
  98. )
  99. .first()
  100. )
  101. return u.id if u else None
  102. def resolve_dept_leader(
  103. db: Session, *, tenant_no: str, initiator_dept_id: Optional[str]
  104. ) -> Tuple[List[str], Dict[str, Any]]:
  105. dept_id = (initiator_dept_id or "").strip()
  106. if not dept_id:
  107. raise AssigneeResolveError("发起人部门为空,无法解析部门负责人")
  108. dept = (
  109. db.query(SysDept)
  110. .filter(SysDept.id == dept_id, SysDept.tenant_no == tenant_no, SysDept.del_flag == 0)
  111. .first()
  112. )
  113. if not dept:
  114. raise AssigneeResolveError("发起人部门不存在")
  115. leader_uid = _active_user_id(db, getattr(dept, "leader_user_id", None), tenant_no)
  116. if not leader_uid:
  117. raise AssigneeResolveError("部门负责人未配置或用户无效,请在组织管理中维护负责人")
  118. return [leader_uid], {"resolve_source": "dept_leader"}
  119. def resolve_direct_supervisor(
  120. db: Session,
  121. *,
  122. tenant_no: str,
  123. initiator_user_id: str,
  124. initiator_dept_id: Optional[str],
  125. fallback_to_dept_leader: bool = True,
  126. ) -> Tuple[List[str], Dict[str, Any]]:
  127. initiator = (
  128. db.query(SysUser)
  129. .filter(
  130. SysUser.id == initiator_user_id,
  131. SysUser.tenant_no == tenant_no,
  132. SysUser.del_flag == 0,
  133. )
  134. .first()
  135. )
  136. if not initiator:
  137. raise AssigneeResolveError("发起人不存在")
  138. supervisor_uid = _active_user_id(
  139. db, getattr(initiator, "supervisor_user_id", None), tenant_no
  140. )
  141. if supervisor_uid:
  142. # 允许主管配置为本人(如部门经理),由高级设置 autoPassWhenApproverIsInitiator 决定是否自动通过
  143. return [supervisor_uid], {"resolve_source": "direct_supervisor"}
  144. if fallback_to_dept_leader:
  145. users, meta = resolve_dept_leader(
  146. db, tenant_no=tenant_no, initiator_dept_id=initiator_dept_id or initiator.dept_id
  147. )
  148. meta = {**meta, "resolve_source": "supervisor_fallback_dept_leader"}
  149. return users, meta
  150. raise AssigneeResolveError("未配置直属主管,且未开启回退部门负责人")
  151. def resolve_assignees(
  152. db: Session,
  153. *,
  154. tenant_no: str,
  155. assignee_config: Dict[str, Any],
  156. initiator_user_id: str,
  157. initiator_dept_id: Optional[str] = None,
  158. advanced: Optional[Dict[str, Any]] = None,
  159. initiator_select_user_ids: Optional[List[str]] = None,
  160. ) -> Tuple[List[str], Dict[str, Any]]:
  161. """
  162. 按节点 approverConfig 解析处理人。
  163. assigneeType: users | posts | dept_leader | direct_supervisor | initiator_select
  164. """
  165. cfg = assignee_config or {}
  166. assignee_type = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip()
  167. advanced = advanced or {}
  168. fallback = advanced.get("supervisorFallbackToDeptLeader", True)
  169. if isinstance(fallback, str):
  170. fallback = fallback.lower() in ("1", "true", "yes")
  171. if assignee_type == "users":
  172. ids = cfg.get("userIds") or cfg.get("user_ids") or []
  173. out: List[str] = []
  174. seen = set()
  175. for uid in ids:
  176. real = _active_user_id(db, uid, tenant_no)
  177. if real and real not in seen:
  178. seen.add(real)
  179. out.append(real)
  180. if not out:
  181. raise AssigneeResolveError("指定成员为空或均无效")
  182. return out, {"resolve_source": "users"}
  183. if assignee_type == "posts":
  184. post_ids = cfg.get("postIds") or cfg.get("post_ids") or []
  185. users = user_crud.list_user_ids_bound_to_posts(db, list(post_ids), tenant_no)
  186. out = []
  187. seen = set()
  188. for uid in users:
  189. real = _active_user_id(db, uid, tenant_no)
  190. if real and real not in seen:
  191. seen.add(real)
  192. out.append(real)
  193. if not out:
  194. raise AssigneeResolveError("职位成员为空或均无效")
  195. return out, {"resolve_source": "posts"}
  196. if assignee_type == "dept_leader":
  197. return resolve_dept_leader(
  198. db, tenant_no=tenant_no, initiator_dept_id=initiator_dept_id
  199. )
  200. if assignee_type == "direct_supervisor":
  201. return resolve_direct_supervisor(
  202. db,
  203. tenant_no=tenant_no,
  204. initiator_user_id=initiator_user_id,
  205. initiator_dept_id=initiator_dept_id,
  206. fallback_to_dept_leader=bool(fallback),
  207. )
  208. if assignee_type == "initiator_select":
  209. ids = initiator_select_user_ids or cfg.get("userIds") or []
  210. out = []
  211. seen = set()
  212. for uid in ids:
  213. real = _active_user_id(db, uid, tenant_no)
  214. if real and real not in seen:
  215. seen.add(real)
  216. out.append(real)
  217. if not out:
  218. raise AssigneeResolveError("发起人未选择审批人")
  219. return out, {"resolve_source": "initiator_select"}
  220. raise AssigneeResolveError(f"不支持的审批人类型: {assignee_type or '(空)'}")