mini_menu.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. """小程序菜单:当前用户在「移动端菜单」树下有权限的入口。"""
  2. from __future__ import annotations
  3. from typing import Dict, List, Set
  4. from fastapi import APIRouter, Depends, Request
  5. from sqlalchemy.orm import Session
  6. from app.core.auth import get_current_user_by_token
  7. from app.core.config import settings
  8. from app.core.database import get_db
  9. from app.core.response import error_response, success_response
  10. from app.models.models import SysMenu, SysRoleMenu
  11. from app.services.web_auth_roles import get_effective_role_ids, resolve_web_role_for_user
  12. router = APIRouter(prefix="/mini", tags=["小程序菜单"])
  13. async def _current_user(request: Request, db: Session) -> dict:
  14. authorization = request.headers.get("Authorization", "")
  15. if not authorization.startswith("Bearer "):
  16. raise ValueError("未授权")
  17. token = authorization.replace("Bearer ", "", 1).strip()
  18. return await get_current_user_by_token(token=token, db=db)
  19. def _find_mobile_root(db: Session) -> SysMenu | None:
  20. tenant = settings.DEFAULT_TENANT_NO
  21. rows = (
  22. db.query(SysMenu)
  23. .filter(
  24. SysMenu.del_flag == 0,
  25. SysMenu.parent_id == "0",
  26. SysMenu.tenant_no == tenant,
  27. )
  28. .all()
  29. )
  30. for m in rows:
  31. name = (m.menu_name or "").strip()
  32. if "移动端" in name:
  33. return m
  34. return None
  35. def _collect_descendants(db: Session, root_id: str) -> List[SysMenu]:
  36. tenant = settings.DEFAULT_TENANT_NO
  37. all_menus = (
  38. db.query(SysMenu)
  39. .filter(
  40. SysMenu.del_flag == 0,
  41. SysMenu.tenant_no == tenant,
  42. SysMenu.status == "1",
  43. SysMenu.visible == "1",
  44. )
  45. .order_by(SysMenu.order_num.asc(), SysMenu.create_time.asc())
  46. .all()
  47. )
  48. by_parent: Dict[str, List[SysMenu]] = {}
  49. for m in all_menus:
  50. pid = (m.parent_id or "0").strip() or "0"
  51. by_parent.setdefault(pid, []).append(m)
  52. out: List[SysMenu] = []
  53. def walk(pid: str) -> None:
  54. for child in by_parent.get(pid, []):
  55. out.append(child)
  56. walk(child.id)
  57. walk(root_id)
  58. return out
  59. def _user_menu_ids(db: Session, user_id: str) -> Set[str]:
  60. role_ids = get_effective_role_ids(db, user_id)
  61. if not role_ids:
  62. return set()
  63. tenant = settings.DEFAULT_TENANT_NO
  64. rows = (
  65. db.query(SysRoleMenu.menu_id)
  66. .filter(
  67. SysRoleMenu.del_flag == 0,
  68. SysRoleMenu.tenant_no == tenant,
  69. SysRoleMenu.role_id.in_(role_ids),
  70. )
  71. .all()
  72. )
  73. return {str(r[0]).strip() for r in rows if r and r[0]}
  74. def _mobile_menu_icon(path: str) -> str:
  75. """按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。"""
  76. p = (path or "").lower()
  77. if "oa/start" in p:
  78. return "approve"
  79. if "oa/drafts" in p:
  80. return "oa-drafts"
  81. if "oa/pending" in p:
  82. return "oa-pending"
  83. if "oa/done" in p:
  84. return "oa-done"
  85. if "oa/mine" in p:
  86. return "oa-mine"
  87. if "oa/cc" in p:
  88. return "oa-cc"
  89. if "oa/mentions" in p:
  90. return "oa-mentions"
  91. if "oa/wechat-notify" in p:
  92. return "oa-wechat-notify"
  93. if "form-sink" in p:
  94. return "orders-o"
  95. return "apps-o"
  96. def _mobile_menu_icon_colors(path: str) -> dict:
  97. """宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。"""
  98. p = (path or "").lower()
  99. # OA 菜单 PNG 自带颜色,宫格不套色底
  100. if any(
  101. x in p
  102. for x in (
  103. "oa/start",
  104. "oa/drafts",
  105. "oa/pending",
  106. "oa/done",
  107. "oa/mine",
  108. "oa/cc",
  109. "oa/mentions",
  110. "oa/wechat-notify",
  111. )
  112. ):
  113. return {"iconBg": "transparent", "iconFg": ""}
  114. if "form-sink" in p:
  115. return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
  116. return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
  117. @router.get("/menus")
  118. async def list_my_mobile_menus(request: Request, db: Session = Depends(get_db)):
  119. try:
  120. user = await _current_user(request, db)
  121. except Exception:
  122. return error_response(401, "未授权")
  123. user_id = str(user.get("user_id") or user.get("sub") or "").strip()
  124. if not user_id:
  125. return error_response(401, "未授权")
  126. root = _find_mobile_root(db)
  127. if not root:
  128. return success_response({"list": []})
  129. descendants = _collect_descendants(db, root.id)
  130. web_role = resolve_web_role_for_user(db, user_id)
  131. allowed_ids = None if web_role == "super_admin" else _user_menu_ids(db, user_id)
  132. items = []
  133. for m in descendants:
  134. if (m.menu_type or "").upper() == "F":
  135. continue
  136. path = (m.path or "").strip()
  137. if not path:
  138. continue
  139. if "form-sink" in path.lower() or "水槽" in str(m.menu_name or ""):
  140. continue
  141. if allowed_ids is not None and m.id not in allowed_ids:
  142. continue
  143. colors = _mobile_menu_icon_colors(path)
  144. items.append(
  145. {
  146. "id": m.id,
  147. "title": m.menu_name,
  148. "path": path if path.startswith("/") else f"/{path}",
  149. "icon": _mobile_menu_icon(path),
  150. "iconBg": colors["iconBg"],
  151. "iconFg": colors["iconFg"],
  152. "perms": (m.perms or "").strip() or None,
  153. }
  154. )
  155. return success_response({"list": items})