role.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. """角色 CRUD(sys_role / sys_role_menu / sys_role_data_scope_detail)"""
  2. from __future__ import annotations
  3. import math
  4. from datetime import datetime
  5. from typing import Any, Dict, List, Optional, Tuple
  6. from sqlalchemy import and_, func
  7. from sqlalchemy.orm import Session
  8. from app.core.utils import generate_uuid
  9. from app.models.models import (
  10. SysPost,
  11. SysPostRole,
  12. SysRole,
  13. SysRoleDataScopeDetail,
  14. SysRoleMenu,
  15. SysUser,
  16. SysUserPost,
  17. SysUserRole,
  18. )
  19. # ---------------------------------------------------------------------------
  20. # helpers
  21. # ---------------------------------------------------------------------------
  22. def _user_display_name(nick_name: Optional[str], user_no: Optional[str]) -> str:
  23. n = (nick_name or "").strip()
  24. if n:
  25. return n
  26. return (user_no or "").strip()
  27. def _append_unique(out: Dict[str, List[str]], key: str, value: str) -> None:
  28. if not key or not value:
  29. return
  30. bucket = out.setdefault(key, [])
  31. if value not in bucket:
  32. bucket.append(value)
  33. def _bound_user_names_by_role_ids(
  34. db: Session, role_ids: List[str], tenant_no: str
  35. ) -> Dict[str, List[str]]:
  36. """批量查询有效拥有角色的用户显示名(直接绑定 ∪ 启用职位继承)。"""
  37. out: Dict[str, List[str]] = {}
  38. ids = [rid for rid in (role_ids or []) if rid]
  39. tenant = (tenant_no or "").strip()
  40. if not ids or not tenant:
  41. return out
  42. direct_rows = (
  43. db.query(SysUserRole.role_id, SysUser.nick_name, SysUser.user_no)
  44. .join(
  45. SysUser,
  46. and_(SysUser.id == SysUserRole.user_id, SysUser.del_flag == 0),
  47. )
  48. .join(
  49. SysRole,
  50. and_(
  51. SysRole.id == SysUserRole.role_id,
  52. SysRole.del_flag == 0,
  53. SysRole.status == "1",
  54. SysRole.tenant_no == tenant,
  55. ),
  56. )
  57. .filter(
  58. and_(
  59. SysUserRole.role_id.in_(ids),
  60. SysUserRole.tenant_no == tenant,
  61. SysUserRole.del_flag == 0,
  62. )
  63. )
  64. .order_by(SysUser.nick_name.asc(), SysUser.user_no.asc())
  65. .all()
  66. )
  67. for role_id, nick_name, user_no in direct_rows:
  68. _append_unique(out, role_id, _user_display_name(nick_name, user_no))
  69. post_rows = (
  70. db.query(SysPostRole.role_id, SysUser.nick_name, SysUser.user_no)
  71. .join(
  72. SysPost,
  73. and_(
  74. SysPost.id == SysPostRole.post_id,
  75. SysPost.del_flag == 0,
  76. SysPost.status == "1",
  77. SysPost.tenant_no == tenant,
  78. ),
  79. )
  80. .join(
  81. SysUserPost,
  82. and_(
  83. SysUserPost.post_id == SysPost.id,
  84. SysUserPost.del_flag == 0,
  85. SysUserPost.tenant_no == tenant,
  86. ),
  87. )
  88. .join(
  89. SysUser,
  90. and_(SysUser.id == SysUserPost.user_id, SysUser.del_flag == 0),
  91. )
  92. .join(
  93. SysRole,
  94. and_(
  95. SysRole.id == SysPostRole.role_id,
  96. SysRole.del_flag == 0,
  97. SysRole.status == "1",
  98. SysRole.tenant_no == tenant,
  99. ),
  100. )
  101. .filter(
  102. and_(
  103. SysPostRole.role_id.in_(ids),
  104. SysPostRole.tenant_no == tenant,
  105. SysPostRole.del_flag == 0,
  106. )
  107. )
  108. .order_by(SysUser.nick_name.asc(), SysUser.user_no.asc())
  109. .all()
  110. )
  111. for role_id, nick_name, user_no in post_rows:
  112. _append_unique(out, role_id, _user_display_name(nick_name, user_no))
  113. return out
  114. def _bound_post_names_by_role_ids(
  115. db: Session, role_ids: List[str], tenant_no: str
  116. ) -> Dict[str, List[str]]:
  117. """批量查询角色关联的启用职位名称。"""
  118. out: Dict[str, List[str]] = {}
  119. ids = [rid for rid in (role_ids or []) if rid]
  120. tenant = (tenant_no or "").strip()
  121. if not ids or not tenant:
  122. return out
  123. rows = (
  124. db.query(SysPostRole.role_id, SysPost.post_name)
  125. .join(
  126. SysPost,
  127. and_(
  128. SysPost.id == SysPostRole.post_id,
  129. SysPost.del_flag == 0,
  130. SysPost.status == "1",
  131. SysPost.tenant_no == tenant,
  132. ),
  133. )
  134. .filter(
  135. and_(
  136. SysPostRole.role_id.in_(ids),
  137. SysPostRole.tenant_no == tenant,
  138. SysPostRole.del_flag == 0,
  139. )
  140. )
  141. .order_by(SysPost.post_name.asc())
  142. .all()
  143. )
  144. for role_id, post_name in rows:
  145. name = (post_name or "").strip()
  146. _append_unique(out, role_id, name)
  147. return out
  148. def _row_to_dict(
  149. r: SysRole,
  150. *,
  151. bound_user_names: Optional[List[str]] = None,
  152. bound_post_names: Optional[List[str]] = None,
  153. ) -> Dict[str, Any]:
  154. return {
  155. "id": r.id,
  156. "roleName": r.role_name,
  157. "dataScope": r.data_scope or "1",
  158. "status": r.status or "1",
  159. "tenantNo": r.tenant_no or "",
  160. "orderNum": r.order_num if r.order_num is not None else 0,
  161. "remark": r.remark or "",
  162. "boundUserNames": list(bound_user_names or []),
  163. "boundPostNames": list(bound_post_names or []),
  164. "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
  165. "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
  166. }
  167. def total_pages(total: int, page_size: int) -> int:
  168. if total <= 0:
  169. return 0
  170. return int(math.ceil(total / float(page_size)))
  171. # ---------------------------------------------------------------------------
  172. # list / get
  173. # ---------------------------------------------------------------------------
  174. def list_roles(
  175. db: Session,
  176. *,
  177. page: int = 1,
  178. page_size: int = 10,
  179. keyword: Optional[str] = None,
  180. tenant_no: Optional[str] = None,
  181. user_info=None,
  182. ) -> Tuple[List[Dict[str, Any]], int]:
  183. def _filtered():
  184. q0 = db.query(SysRole).filter(SysRole.del_flag == 0)
  185. if tenant_no:
  186. q0 = q0.filter(SysRole.tenant_no == tenant_no)
  187. kw = (keyword or "").strip()
  188. if kw:
  189. like = f"%{kw}%"
  190. q0 = q0.filter(SysRole.role_name.like(like))
  191. if user_info:
  192. from app.core.data_scope import apply_data_scope
  193. q0 = apply_data_scope(q0, SysRole, user_info, db, tenant_no)
  194. return q0
  195. total = _filtered().order_by(None).with_entities(func.count(SysRole.id)).scalar() or 0
  196. page = max(1, page)
  197. page_size = min(max(1, page_size), 100)
  198. rows = (
  199. _filtered()
  200. .order_by(SysRole.create_time.desc())
  201. .offset((page - 1) * page_size)
  202. .limit(page_size)
  203. .all()
  204. )
  205. role_ids = [x.id for x in rows]
  206. tenant = tenant_no or ""
  207. user_name_map = _bound_user_names_by_role_ids(db, role_ids, tenant)
  208. post_name_map = _bound_post_names_by_role_ids(db, role_ids, tenant)
  209. return [
  210. _row_to_dict(
  211. x,
  212. bound_user_names=user_name_map.get(x.id, []),
  213. bound_post_names=post_name_map.get(x.id, []),
  214. )
  215. for x in rows
  216. ], int(total)
  217. def get_role(db: Session, role_id: str) -> Optional[SysRole]:
  218. return (
  219. db.query(SysRole)
  220. .filter(and_(SysRole.id == role_id, SysRole.del_flag == 0))
  221. .first()
  222. )
  223. def role_name_exists(db: Session, name: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool:
  224. q = db.query(SysRole).filter(
  225. and_(SysRole.role_name == name.strip(), SysRole.tenant_no == tenant_no, SysRole.del_flag == 0)
  226. )
  227. if exclude_id:
  228. q = q.filter(SysRole.id != exclude_id)
  229. return q.first() is not None
  230. # ---------------------------------------------------------------------------
  231. # create / update / delete
  232. # ---------------------------------------------------------------------------
  233. def create_role(
  234. db: Session,
  235. *,
  236. role_name: str,
  237. data_scope: str = "4",
  238. status: str = "1",
  239. tenant_no: str = "zkxw",
  240. order_num: Optional[int] = None,
  241. remark: Optional[str] = None,
  242. user_id: Optional[str] = None,
  243. ) -> SysRole:
  244. rn = (role_name or "").strip()
  245. if not rn:
  246. raise ValueError("角色名称不能为空")
  247. if role_name_exists(db, rn, tenant_no):
  248. raise ValueError("角色名称已存在")
  249. obj = SysRole(
  250. id=generate_uuid(),
  251. role_name=rn,
  252. data_scope=data_scope,
  253. status=status,
  254. tenant_no=tenant_no,
  255. order_num=order_num if order_num is not None else 0,
  256. remark=(remark or "").strip() or None,
  257. create_by=user_id,
  258. create_dept_id="",
  259. create_time=datetime.now(),
  260. update_by=user_id,
  261. update_time=datetime.now(),
  262. del_flag=0,
  263. )
  264. db.add(obj)
  265. db.commit()
  266. db.refresh(obj)
  267. return obj
  268. def update_role(
  269. db: Session,
  270. role_id: str,
  271. *,
  272. role_name: Optional[str] = None,
  273. data_scope: Optional[str] = None,
  274. status: Optional[str] = None,
  275. order_num: Optional[int] = None,
  276. remark: Optional[str] = None,
  277. user_id: Optional[str] = None,
  278. ) -> SysRole:
  279. obj = get_role(db, role_id)
  280. if not obj:
  281. raise ValueError("角色不存在")
  282. if role_name is not None:
  283. rn = role_name.strip()
  284. if not rn:
  285. raise ValueError("角色名称不能为空")
  286. if rn != obj.role_name and role_name_exists(db, rn, obj.tenant_no, exclude_id=role_id):
  287. raise ValueError("角色名称已存在")
  288. obj.role_name = rn
  289. if data_scope is not None:
  290. obj.data_scope = data_scope
  291. if status is not None:
  292. obj.status = status
  293. if order_num is not None:
  294. obj.order_num = order_num
  295. if remark is not None:
  296. obj.remark = (remark or "").strip() or None
  297. obj.update_by = user_id
  298. obj.update_time = datetime.now()
  299. db.add(obj)
  300. db.commit()
  301. db.refresh(obj)
  302. return obj
  303. def soft_delete_role(db: Session, role_id: str) -> None:
  304. obj = get_role(db, role_id)
  305. if not obj:
  306. raise ValueError("角色不存在")
  307. obj.del_flag = 1
  308. obj.update_time = datetime.now()
  309. db.add(obj)
  310. # cascade soft-delete role_menu and role_data_scope_detail
  311. db.query(SysRoleMenu).filter(
  312. and_(SysRoleMenu.role_id == role_id, SysRoleMenu.del_flag == 0)
  313. ).update({"del_flag": 1, "update_time": datetime.now()})
  314. db.query(SysRoleDataScopeDetail).filter(
  315. and_(SysRoleDataScopeDetail.role_id == role_id, SysRoleDataScopeDetail.del_flag == 0)
  316. ).update({"del_flag": 1, "update_time": datetime.now()})
  317. db.commit()
  318. # ---------------------------------------------------------------------------
  319. # role-menu
  320. # ---------------------------------------------------------------------------
  321. def get_role_menu_ids(db: Session, role_id: str) -> List[str]:
  322. rows = (
  323. db.query(SysRoleMenu.menu_id)
  324. .filter(and_(SysRoleMenu.role_id == role_id, SysRoleMenu.del_flag == 0))
  325. .all()
  326. )
  327. return [r.menu_id for r in rows]
  328. def update_role_menus(
  329. db: Session,
  330. role_id: str,
  331. menu_ids: List[str],
  332. tenant_no: str,
  333. user_id: Optional[str] = None,
  334. ) -> None:
  335. # uk_role_menu 为 (role_id, menu_id, del_flag):先全量软删再插入会在再次保存时
  336. # 出现两条 (role_id, menu_id, del_flag=1),触发 1062。关联表采用整角色物理清理后重建。
  337. unique_ids = list(dict.fromkeys(menu_ids or []))
  338. db.query(SysRoleMenu).filter(SysRoleMenu.role_id == role_id).delete(synchronize_session=False)
  339. now = datetime.now()
  340. for mid in unique_ids:
  341. obj = SysRoleMenu(
  342. id=generate_uuid(),
  343. role_id=role_id,
  344. menu_id=mid,
  345. tenant_no=tenant_no,
  346. order_num=0,
  347. create_by=user_id,
  348. create_dept_id="",
  349. create_time=now,
  350. update_by=user_id,
  351. update_time=now,
  352. del_flag=0,
  353. )
  354. db.add(obj)
  355. db.commit()
  356. # ---------------------------------------------------------------------------
  357. # role data-scope
  358. # ---------------------------------------------------------------------------
  359. def get_role_data_scope(db: Session, role_id: str) -> List[Dict[str, str]]:
  360. rows = (
  361. db.query(SysRoleDataScopeDetail)
  362. .filter(and_(SysRoleDataScopeDetail.role_id == role_id, SysRoleDataScopeDetail.del_flag == 0))
  363. .all()
  364. )
  365. return [{"scopeType": r.scope_type, "scopeValue": r.scope_value} for r in rows]
  366. def update_role_data_scope(
  367. db: Session,
  368. role_id: str,
  369. data_scope: str,
  370. scope_details: Optional[List[Dict[str, str]]] = None,
  371. tenant_no: str = "zkxw",
  372. user_id: Optional[str] = None,
  373. ) -> None:
  374. obj = get_role(db, role_id)
  375. if not obj:
  376. raise ValueError("角色不存在")
  377. obj.data_scope = data_scope
  378. obj.update_by = user_id
  379. obj.update_time = datetime.now()
  380. db.add(obj)
  381. # soft-delete old scope details
  382. db.query(SysRoleDataScopeDetail).filter(
  383. and_(SysRoleDataScopeDetail.role_id == role_id, SysRoleDataScopeDetail.del_flag == 0)
  384. ).update({"del_flag": 1, "update_time": datetime.now()})
  385. # insert new scope details (only when custom data_scope == '5')
  386. if data_scope == "5" and scope_details:
  387. now = datetime.now()
  388. for detail in scope_details:
  389. sd = SysRoleDataScopeDetail(
  390. id=generate_uuid(),
  391. role_id=role_id,
  392. scope_type=detail.get("scopeType", "dept"),
  393. scope_value=detail.get("scopeValue", ""),
  394. tenant_no=tenant_no,
  395. order_num=0,
  396. create_by=user_id,
  397. create_dept_id="",
  398. create_time=now,
  399. update_by=user_id,
  400. update_time=now,
  401. del_flag=0,
  402. )
  403. db.add(sd)
  404. db.commit()