menu.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. from __future__ import annotations
  2. from datetime import datetime
  3. from typing import Any, Dict, List, Optional
  4. from sqlalchemy import and_
  5. from sqlalchemy.orm import Session
  6. from app.core.delete_guard import assert_row_deletable_by_creator
  7. from app.core.utils import generate_uuid
  8. from app.models.models import SysMenu
  9. def _row_to_dict(m: SysMenu) -> Dict[str, Any]:
  10. return {
  11. "id": m.id,
  12. "menuName": m.menu_name,
  13. "parentId": m.parent_id,
  14. "menuType": m.menu_type or "M",
  15. "path": m.path or "",
  16. "component": m.component or "",
  17. "perms": m.perms or "",
  18. "isFrame": m.is_frame if m.is_frame is not None else 1,
  19. "visible": m.visible or "1",
  20. "status": m.status or "1",
  21. "tenantNo": m.tenant_no,
  22. "orderNum": m.order_num if m.order_num is not None else 0,
  23. "remark": m.remark or "",
  24. "createBy": m.create_by or "",
  25. "createTime": m.create_time.strftime("%Y-%m-%d %H:%M:%S") if m.create_time else "",
  26. "updateTime": m.update_time.strftime("%Y-%m-%d %H:%M:%S") if m.update_time else "",
  27. }
  28. def _build_tree(items: List[Dict[str, Any]], parent_id: str = "0") -> List[Dict[str, Any]]:
  29. children = [item for item in items if item.get("parentId") == parent_id]
  30. for child in children:
  31. child["children"] = _build_tree(items, child["id"])
  32. children.sort(key=lambda x: (x.get("orderNum", 0) or 0))
  33. return children
  34. def list_menus(
  35. db: Session,
  36. *,
  37. keyword: Optional[str] = None,
  38. tenant_no: Optional[str] = None,
  39. user_info=None,
  40. ) -> List[Dict[str, Any]]:
  41. q = db.query(SysMenu).filter(SysMenu.del_flag == 0)
  42. if tenant_no:
  43. q = q.filter(SysMenu.tenant_no == tenant_no)
  44. kw = (keyword or "").strip()
  45. if kw:
  46. like = f"%{kw}%"
  47. q = q.filter(SysMenu.menu_name.like(like))
  48. if user_info:
  49. from app.core.data_scope import apply_data_scope
  50. q = apply_data_scope(q, SysMenu, user_info, db, tenant_no)
  51. rows = q.order_by(SysMenu.create_time.desc()).all()
  52. return [_row_to_dict(x) for x in rows]
  53. def list_menus_tree(
  54. db: Session,
  55. *,
  56. keyword: Optional[str] = None,
  57. tenant_no: Optional[str] = None,
  58. user_info=None,
  59. ) -> List[Dict[str, Any]]:
  60. kw = (keyword or "").strip()
  61. # 关键词检索时:仅返回命中节点会导致其父节点缺失,从而无法组装为以 parentId=0 开始的树。
  62. # 这里改为:命中节点 + 其所有祖先节点,再组树。
  63. if not kw:
  64. flat = list_menus(db, keyword=None, tenant_no=tenant_no, user_info=user_info)
  65. return _build_tree(flat, "0")
  66. all_items = list_menus(db, keyword=None, tenant_no=tenant_no, user_info=user_info)
  67. id_map: Dict[str, Dict[str, Any]] = {x.get("id"): x for x in all_items if x.get("id")}
  68. like_items = [
  69. x for x in all_items if kw and kw in (x.get("menuName") or "")
  70. ]
  71. picked: Dict[str, Dict[str, Any]] = {}
  72. def pick_with_ancestors(item_id: str) -> None:
  73. cur = id_map.get(item_id)
  74. while cur and cur.get("id") and cur.get("id") not in picked:
  75. picked[cur["id"]] = cur
  76. parent_id = (cur.get("parentId") or "0").strip()
  77. if not parent_id or parent_id == "0":
  78. break
  79. cur = id_map.get(parent_id)
  80. for it in like_items:
  81. if it.get("id"):
  82. pick_with_ancestors(it["id"])
  83. return _build_tree(list(picked.values()), "0")
  84. def get_menu(db: Session, menu_id: str) -> Optional[SysMenu]:
  85. return (
  86. db.query(SysMenu)
  87. .filter(and_(SysMenu.id == menu_id, SysMenu.del_flag == 0))
  88. .first()
  89. )
  90. def menu_name_exists(
  91. db: Session,
  92. name: str,
  93. tenant_no: str,
  94. exclude_id: Optional[str] = None,
  95. ) -> bool:
  96. q = db.query(SysMenu).filter(
  97. and_(SysMenu.menu_name == name, SysMenu.tenant_no == tenant_no, SysMenu.del_flag == 0)
  98. )
  99. if exclude_id:
  100. q = q.filter(SysMenu.id != exclude_id)
  101. return q.first() is not None
  102. def create_menu(
  103. db: Session,
  104. *,
  105. menu_name: str,
  106. parent_id: str = "0",
  107. menu_type: str = "M",
  108. path: str = "",
  109. component: Optional[str] = None,
  110. perms: Optional[str] = None,
  111. is_frame: int = 1,
  112. visible: str = "1",
  113. status: str = "1",
  114. tenant_no: Optional[str] = None,
  115. order_num: Optional[int] = None,
  116. remark: Optional[str] = None,
  117. user_id: Optional[str] = None,
  118. ) -> SysMenu:
  119. name = (menu_name or "").strip()
  120. if not name:
  121. raise ValueError("菜单名称不能为空")
  122. obj = SysMenu(
  123. id=generate_uuid(),
  124. menu_name=name,
  125. parent_id=(parent_id or "0").strip(),
  126. menu_type=menu_type or "M",
  127. path=(path or "").strip(),
  128. component=(component or "").strip() or None,
  129. perms=(perms or "").strip() or None,
  130. is_frame=is_frame if is_frame is not None else 1,
  131. visible=visible or "1",
  132. status=status or "1",
  133. tenant_no=tenant_no or "shanxia2.0",
  134. order_num=order_num if order_num is not None else 0,
  135. remark=(remark or "").strip() or None,
  136. create_by=user_id,
  137. create_dept_id="",
  138. create_time=datetime.now(),
  139. update_by=user_id,
  140. update_time=datetime.now(),
  141. del_flag=0,
  142. )
  143. db.add(obj)
  144. db.commit()
  145. db.refresh(obj)
  146. return obj
  147. def update_menu(
  148. db: Session,
  149. menu_id: str,
  150. *,
  151. menu_name: Optional[str] = None,
  152. parent_id: Optional[str] = None,
  153. menu_type: Optional[str] = None,
  154. path: Optional[str] = None,
  155. component: Optional[str] = None,
  156. perms: Optional[str] = None,
  157. is_frame: Optional[int] = None,
  158. visible: Optional[str] = None,
  159. status: Optional[str] = None,
  160. order_num: Optional[int] = None,
  161. remark: Optional[str] = None,
  162. user_id: Optional[str] = None,
  163. ) -> SysMenu:
  164. obj = get_menu(db, menu_id)
  165. if not obj:
  166. raise ValueError("菜单不存在")
  167. if menu_name is not None:
  168. obj.menu_name = (menu_name or "").strip() or obj.menu_name
  169. if parent_id is not None:
  170. obj.parent_id = (parent_id or "0").strip()
  171. if menu_type is not None:
  172. obj.menu_type = menu_type
  173. if path is not None:
  174. obj.path = (path or "").strip()
  175. if component is not None:
  176. obj.component = (component or "").strip() or None
  177. if perms is not None:
  178. obj.perms = (perms or "").strip() or None
  179. if is_frame is not None:
  180. obj.is_frame = is_frame
  181. if visible is not None:
  182. obj.visible = visible
  183. if status is not None:
  184. obj.status = status
  185. if order_num is not None:
  186. obj.order_num = order_num
  187. if remark is not None:
  188. obj.remark = (remark or "").strip() or None
  189. obj.update_by = user_id
  190. obj.update_time = datetime.now()
  191. db.add(obj)
  192. db.commit()
  193. db.refresh(obj)
  194. return obj
  195. def _cascade_soft_delete(db: Session, menu_id: str) -> None:
  196. children = (
  197. db.query(SysMenu)
  198. .filter(and_(SysMenu.parent_id == menu_id, SysMenu.del_flag == 0))
  199. .all()
  200. )
  201. for child in children:
  202. _cascade_soft_delete(db, child.id)
  203. child.del_flag = 1
  204. child.update_time = datetime.now()
  205. db.add(child)
  206. def soft_delete_menu(db: Session, menu_id: str, *, user_info: dict = None) -> None:
  207. obj = get_menu(db, menu_id)
  208. if not obj:
  209. raise ValueError("菜单不存在")
  210. assert_row_deletable_by_creator(obj, user_info)
  211. _cascade_soft_delete(db, menu_id)
  212. obj.del_flag = 1
  213. obj.update_time = datetime.now()
  214. db.add(obj)
  215. db.commit()