web_auth_roles.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. """Web 管理端登录:基于 V0.3 sys_user / sys_user_role / sys_role 的角色与菜单权限解析
  2. 有效角色 = 用户直接角色 ∪ 经启用职位继承的角色(sys_post_role)。
  3. """
  4. from __future__ import annotations
  5. from typing import List, Optional, Set, Collection
  6. from sqlalchemy import and_
  7. from sqlalchemy.orm import Session
  8. from app.core.config import settings
  9. from app.models.models import (
  10. SysMenu,
  11. SysPost,
  12. SysPostRole,
  13. SysRole,
  14. SysRoleMenu,
  15. SysUserPost,
  16. SysUserRole,
  17. )
  18. # 无有效角色绑定时的 Web 端角色(不可为 super_admin,避免越权)
  19. NO_ROLE_WEB_ROLE = "guest"
  20. # 一级菜单权限码(用于顶部菜单权限控制)
  21. TOP_MENU_PERMS = frozenset({"menu:market", "menu:production", "menu:quality", "menu:operations", "menu:system-manage"})
  22. def _map_role_name_to_web(role_name: str) -> str:
  23. """将 sys_role.role_name 映射为 web-admin 使用的 role 字符串。"""
  24. n = (role_name or "").strip()
  25. if not n:
  26. return "admin"
  27. if "超级" in n or n in ("系统管理员", "超级管理员"):
  28. return "super_admin"
  29. if "管理" in n or "运营" in n:
  30. return "admin"
  31. return "admin"
  32. def _dedupe_role_id_rows(rows) -> List[str]:
  33. out: List[str] = []
  34. seen: Set[str] = set()
  35. for row in rows:
  36. rid = str(row[0]).strip() if row and row[0] else ""
  37. if rid and rid not in seen:
  38. seen.add(rid)
  39. out.append(rid)
  40. return out
  41. def get_direct_enabled_role_ids(db: Session, user_id: str) -> List[str]:
  42. """用户直接绑定且启用的角色 ID(sys_user_role → sys_role)。"""
  43. if not user_id:
  44. return []
  45. tenant = settings.DEFAULT_TENANT_NO
  46. rows = (
  47. db.query(SysRole.id)
  48. .join(SysUserRole, SysUserRole.role_id == SysRole.id)
  49. .filter(
  50. SysUserRole.user_id == user_id,
  51. SysUserRole.del_flag == 0,
  52. SysUserRole.tenant_no == tenant,
  53. SysRole.del_flag == 0,
  54. SysRole.status == "1",
  55. SysRole.tenant_no == tenant,
  56. )
  57. .order_by(SysRole.create_time.desc())
  58. .all()
  59. )
  60. return _dedupe_role_id_rows(rows)
  61. def get_post_inherited_role_ids(db: Session, user_id: str) -> List[str]:
  62. """经启用职位继承的启用角色 ID(sys_user_post → sys_post → sys_post_role → sys_role)。"""
  63. if not user_id:
  64. return []
  65. tenant = settings.DEFAULT_TENANT_NO
  66. rows = (
  67. db.query(SysRole.id)
  68. .join(SysPostRole, SysPostRole.role_id == SysRole.id)
  69. .join(
  70. SysPost,
  71. and_(
  72. SysPost.id == SysPostRole.post_id,
  73. SysPost.del_flag == 0,
  74. SysPost.status == "1",
  75. SysPost.tenant_no == tenant,
  76. ),
  77. )
  78. .join(
  79. SysUserPost,
  80. and_(
  81. SysUserPost.post_id == SysPost.id,
  82. SysUserPost.user_id == user_id,
  83. SysUserPost.del_flag == 0,
  84. SysUserPost.tenant_no == tenant,
  85. ),
  86. )
  87. .filter(
  88. SysPostRole.del_flag == 0,
  89. SysPostRole.tenant_no == tenant,
  90. SysRole.del_flag == 0,
  91. SysRole.status == "1",
  92. SysRole.tenant_no == tenant,
  93. )
  94. .order_by(SysRole.create_time.desc())
  95. .all()
  96. )
  97. return _dedupe_role_id_rows(rows)
  98. def get_effective_role_ids(db: Session, user_id: str) -> List[str]:
  99. """用户有效角色 ID:直接绑定 ∪ 启用职位关联的启用角色(去重保序)。"""
  100. if not user_id:
  101. return []
  102. out: List[str] = []
  103. seen: Set[str] = set()
  104. for rid in get_direct_enabled_role_ids(db, user_id) + get_post_inherited_role_ids(
  105. db, user_id
  106. ):
  107. if rid not in seen:
  108. seen.add(rid)
  109. out.append(rid)
  110. return out
  111. def _normalize_candidate_user_ids(
  112. candidate_user_ids: Optional[Collection[str]],
  113. ) -> Optional[List[str]]:
  114. if candidate_user_ids is None:
  115. return None
  116. return list(dict.fromkeys([str(u).strip() for u in candidate_user_ids if u and str(u).strip()]))
  117. def list_user_ids_with_effective_role_name(
  118. db: Session,
  119. *,
  120. tenant_no: str,
  121. role_name: str,
  122. candidate_user_ids: Optional[Collection[str]] = None,
  123. ) -> Set[str]:
  124. """拥有指定角色名的用户 ID(直接绑定 ∪ 职位继承)。"""
  125. tenant = (tenant_no or "").strip()
  126. rn = (role_name or "").strip()
  127. if not tenant or not rn:
  128. return set()
  129. candidates = _normalize_candidate_user_ids(candidate_user_ids)
  130. if candidates is not None and not candidates:
  131. return set()
  132. direct_q = (
  133. db.query(SysUserRole.user_id)
  134. .join(SysRole, SysRole.id == SysUserRole.role_id)
  135. .filter(
  136. SysUserRole.del_flag == 0,
  137. SysUserRole.tenant_no == tenant,
  138. SysRole.del_flag == 0,
  139. SysRole.status == "1",
  140. SysRole.tenant_no == tenant,
  141. SysRole.role_name == rn,
  142. )
  143. )
  144. post_q = (
  145. db.query(SysUserPost.user_id)
  146. .join(
  147. SysPost,
  148. and_(
  149. SysPost.id == SysUserPost.post_id,
  150. SysPost.del_flag == 0,
  151. SysPost.status == "1",
  152. SysPost.tenant_no == tenant,
  153. ),
  154. )
  155. .join(
  156. SysPostRole,
  157. and_(
  158. SysPostRole.post_id == SysPost.id,
  159. SysPostRole.del_flag == 0,
  160. SysPostRole.tenant_no == tenant,
  161. ),
  162. )
  163. .join(
  164. SysRole,
  165. and_(
  166. SysRole.id == SysPostRole.role_id,
  167. SysRole.del_flag == 0,
  168. SysRole.status == "1",
  169. SysRole.tenant_no == tenant,
  170. SysRole.role_name == rn,
  171. ),
  172. )
  173. .filter(SysUserPost.del_flag == 0, SysUserPost.tenant_no == tenant)
  174. )
  175. if candidates is not None:
  176. direct_q = direct_q.filter(SysUserRole.user_id.in_(candidates))
  177. post_q = post_q.filter(SysUserPost.user_id.in_(candidates))
  178. out: Set[str] = set()
  179. for row in direct_q.all():
  180. uid = str(row[0]).strip() if row and row[0] else ""
  181. if uid:
  182. out.add(uid)
  183. for row in post_q.all():
  184. uid = str(row[0]).strip() if row and row[0] else ""
  185. if uid:
  186. out.add(uid)
  187. return out
  188. def list_user_ids_with_any_effective_role_ids(
  189. db: Session,
  190. *,
  191. tenant_no: str,
  192. role_ids: List[str],
  193. candidate_user_ids: Optional[Collection[str]] = None,
  194. ) -> List[str]:
  195. """拥有任一指定角色 ID 的用户 ID 列表(直接绑定 ∪ 职位继承,去重)。"""
  196. tenant = (tenant_no or "").strip()
  197. rids = list(dict.fromkeys([str(r).strip() for r in (role_ids or []) if r and str(r).strip()]))
  198. if not tenant or not rids:
  199. return []
  200. candidates = _normalize_candidate_user_ids(candidate_user_ids)
  201. if candidates is not None and not candidates:
  202. return []
  203. direct_q = (
  204. db.query(SysUserRole.user_id)
  205. .join(SysRole, SysRole.id == SysUserRole.role_id)
  206. .filter(
  207. SysUserRole.del_flag == 0,
  208. SysUserRole.tenant_no == tenant,
  209. SysUserRole.role_id.in_(rids),
  210. SysRole.del_flag == 0,
  211. SysRole.status == "1",
  212. SysRole.tenant_no == tenant,
  213. )
  214. )
  215. post_q = (
  216. db.query(SysUserPost.user_id)
  217. .join(
  218. SysPost,
  219. and_(
  220. SysPost.id == SysUserPost.post_id,
  221. SysPost.del_flag == 0,
  222. SysPost.status == "1",
  223. SysPost.tenant_no == tenant,
  224. ),
  225. )
  226. .join(
  227. SysPostRole,
  228. and_(
  229. SysPostRole.post_id == SysPost.id,
  230. SysPostRole.del_flag == 0,
  231. SysPostRole.tenant_no == tenant,
  232. SysPostRole.role_id.in_(rids),
  233. ),
  234. )
  235. .join(
  236. SysRole,
  237. and_(
  238. SysRole.id == SysPostRole.role_id,
  239. SysRole.del_flag == 0,
  240. SysRole.status == "1",
  241. SysRole.tenant_no == tenant,
  242. ),
  243. )
  244. .filter(SysUserPost.del_flag == 0, SysUserPost.tenant_no == tenant)
  245. )
  246. if candidates is not None:
  247. direct_q = direct_q.filter(SysUserRole.user_id.in_(candidates))
  248. post_q = post_q.filter(SysUserPost.user_id.in_(candidates))
  249. out: List[str] = []
  250. seen: Set[str] = set()
  251. for row in list(direct_q.all()) + list(post_q.all()):
  252. uid = str(row[0]).strip() if row and row[0] else ""
  253. if uid and uid not in seen:
  254. seen.add(uid)
  255. out.append(uid)
  256. return out
  257. def get_role_names_by_ids(db: Session, role_ids: List[str]) -> List[str]:
  258. """按给定角色 ID 返回启用角色名称(保序、去重)。"""
  259. ordered = list(dict.fromkeys([str(r).strip() for r in (role_ids or []) if r]))
  260. if not ordered:
  261. return []
  262. tenant = settings.DEFAULT_TENANT_NO
  263. rows = (
  264. db.query(SysRole.id, SysRole.role_name)
  265. .filter(
  266. SysRole.id.in_(ordered),
  267. SysRole.del_flag == 0,
  268. SysRole.status == "1",
  269. SysRole.tenant_no == tenant,
  270. )
  271. .all()
  272. )
  273. name_by_id = {
  274. str(rid).strip(): str(name).strip()
  275. for rid, name in rows
  276. if rid and name
  277. }
  278. out: List[str] = []
  279. seen: Set[str] = set()
  280. for rid in ordered:
  281. name = name_by_id.get(rid)
  282. if name and name not in seen:
  283. seen.add(name)
  284. out.append(name)
  285. return out
  286. def _active_role_name_rows(db: Session, user_id: str) -> list:
  287. """查询用户当前有效的 sys_role.role_name 列表(含职位继承)。"""
  288. return get_role_names_by_ids(db, get_effective_role_ids(db, user_id))
  289. def user_has_active_roles(db: Session, user_id: str) -> bool:
  290. """用户是否至少拥有一个有效角色(直接或经职位继承)。"""
  291. return bool(get_effective_role_ids(db, user_id))
  292. def get_user_role_names(db: Session, user_id: str) -> list:
  293. """用户当前有效的 sys_role.role_name 列表(去重、保序)。"""
  294. names = _active_role_name_rows(db, user_id)
  295. return list(dict.fromkeys(names))
  296. def resolve_web_role_for_user(db: Session, user_id: str) -> str:
  297. """根据用户有效角色解析 Web 端 role(admin / super_admin / guest)。"""
  298. names = _active_role_name_rows(db, user_id)
  299. if not names:
  300. return NO_ROLE_WEB_ROLE
  301. mapped = [_map_role_name_to_web(n) for n in names]
  302. if "super_admin" in mapped:
  303. return "super_admin"
  304. return "admin"
  305. def iter_user_menu_perm_codes(db: Session, user_id: str) -> Set[str]:
  306. """用户通过有效角色关联得到的菜单权限标识(sys_menu.perms,空则退回菜单 id)。
  307. 注:不过滤 visible——F 类型按钮等 visible=0 的菜单仍可通过角色授权生效;
  308. visible 仅控制侧栏/菜单树展示,不影响权限码解析。
  309. 一级菜单(menu:market 等)同样经本函数返回,用于前端顶部菜单权限控制。
  310. """
  311. if not user_id:
  312. return set()
  313. role_ids = get_effective_role_ids(db, user_id)
  314. if not role_ids:
  315. return set()
  316. tenant = settings.DEFAULT_TENANT_NO
  317. try:
  318. q = (
  319. db.query(SysMenu.perms, SysMenu.id)
  320. .join(
  321. SysRoleMenu,
  322. and_(
  323. SysRoleMenu.menu_id == SysMenu.id,
  324. SysRoleMenu.del_flag == 0,
  325. SysRoleMenu.tenant_no == tenant,
  326. SysRoleMenu.role_id.in_(role_ids),
  327. ),
  328. )
  329. .filter(
  330. SysMenu.del_flag == 0,
  331. SysMenu.tenant_no == tenant,
  332. SysMenu.status == "1",
  333. )
  334. .distinct()
  335. )
  336. out: Set[str] = set()
  337. for perms, mid in q.all():
  338. p = (perms or "").strip()
  339. if p:
  340. out.add(p)
  341. elif mid:
  342. out.add(str(mid).strip())
  343. return out
  344. except Exception:
  345. return set()