mini_menu.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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.database import get_db
  8. from app.core.response import error_response, success_response
  9. from app.models.models import SysMenu, SysRoleMenu
  10. from app.services.web_auth_roles import get_effective_role_ids, resolve_web_role_for_user
  11. router = APIRouter(prefix="/mini", tags=["小程序菜单"])
  12. async def _current_user(request: Request, db: Session) -> dict:
  13. authorization = request.headers.get("Authorization", "")
  14. if not authorization.startswith("Bearer "):
  15. raise ValueError("未授权")
  16. token = authorization.replace("Bearer ", "", 1).strip()
  17. return await get_current_user_by_token(token=token, db=db)
  18. def _find_mobile_root(db: Session, tenant_no: str) -> SysMenu | None:
  19. rows = (
  20. db.query(SysMenu)
  21. .filter(
  22. SysMenu.del_flag == 0,
  23. SysMenu.parent_id == "0",
  24. SysMenu.tenant_no == tenant_no,
  25. )
  26. .all()
  27. )
  28. for m in rows:
  29. name = (m.menu_name or "").strip()
  30. if "移动端" in name:
  31. return m
  32. return None
  33. def _collect_descendants(
  34. db: Session, root_id: str, tenant_no: str
  35. ) -> List[SysMenu]:
  36. all_menus = (
  37. db.query(SysMenu)
  38. .filter(
  39. SysMenu.del_flag == 0,
  40. SysMenu.tenant_no == tenant_no,
  41. SysMenu.status == "1",
  42. SysMenu.visible == "1",
  43. )
  44. .order_by(SysMenu.order_num.asc(), SysMenu.create_time.asc())
  45. .all()
  46. )
  47. by_parent: Dict[str, List[SysMenu]] = {}
  48. for m in all_menus:
  49. pid = (m.parent_id or "0").strip() or "0"
  50. by_parent.setdefault(pid, []).append(m)
  51. out: List[SysMenu] = []
  52. def walk(pid: str) -> None:
  53. for child in by_parent.get(pid, []):
  54. out.append(child)
  55. walk(child.id)
  56. walk(root_id)
  57. return out
  58. def _user_menu_ids(db: Session, user_id: str, tenant_no: str) -> Set[str]:
  59. role_ids = get_effective_role_ids(db, user_id, tenant_no=tenant_no)
  60. if not role_ids:
  61. return set()
  62. rows = (
  63. db.query(SysRoleMenu.menu_id)
  64. .filter(
  65. SysRoleMenu.del_flag == 0,
  66. SysRoleMenu.tenant_no == tenant_no,
  67. SysRoleMenu.role_id.in_(role_ids),
  68. )
  69. .all()
  70. )
  71. return {str(r[0]).strip() for r in rows if r and r[0]}
  72. def _mobile_menu_icon(path: str) -> str:
  73. """按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。"""
  74. p = (path or "").lower()
  75. # 兼容有无前导斜杠、query 分隔写法
  76. if "market/webview" in p and "entry=" in p:
  77. # payment-chase 须先于 payment 匹配
  78. if "payment-chase" in p:
  79. return "market-payment-chase"
  80. if "presign" in p:
  81. return "market-presign"
  82. if "protocol" in p:
  83. return "market-protocol"
  84. if "payment" in p:
  85. return "market-payment"
  86. if "freight" in p:
  87. return "market-freight"
  88. return ""
  89. if "oa/start" in p:
  90. return "approve"
  91. if "oa/drafts" in p:
  92. return "oa-drafts"
  93. if "oa/pending" in p:
  94. return "oa-pending"
  95. if "oa/done" in p:
  96. return "oa-done"
  97. if "oa/mine" in p:
  98. return "oa-mine"
  99. if "oa/cc" in p:
  100. return "oa-cc"
  101. if "oa/mentions" in p:
  102. return "oa-mentions"
  103. if "oa/wechat-notify" in p:
  104. return "oa-wechat-notify"
  105. if "form-sink" in p:
  106. return "orders-o"
  107. return "apps-o"
  108. def _mobile_menu_icon_colors(path: str) -> dict:
  109. """宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。"""
  110. p = (path or "").lower()
  111. # OA / 市场菜单 PNG 自带颜色,宫格不套色底
  112. if any(
  113. x in p
  114. for x in (
  115. "oa/start",
  116. "oa/drafts",
  117. "oa/pending",
  118. "oa/done",
  119. "oa/mine",
  120. "oa/cc",
  121. "oa/mentions",
  122. "oa/wechat-notify",
  123. )
  124. ) or ("market/webview" in p and "entry=" in p):
  125. return {"iconBg": "transparent", "iconFg": ""}
  126. if "form-sink" in p:
  127. return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
  128. return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
  129. def list_mobile_menu_items(
  130. db: Session,
  131. *,
  132. user_id: str,
  133. tenant_no: str,
  134. is_super_admin: bool = False,
  135. ) -> List[dict]:
  136. """列出当前租户下已授权的移动端叶子入口。"""
  137. tenant = (tenant_no or "").strip()
  138. uid = (user_id or "").strip()
  139. if not tenant or not uid:
  140. return []
  141. root = _find_mobile_root(db, tenant)
  142. if not root:
  143. return []
  144. descendants = _collect_descendants(db, root.id, tenant)
  145. by_id = {menu.id: menu for menu in descendants}
  146. allowed_ids = None if is_super_admin else _user_menu_ids(db, uid, tenant)
  147. items: List[dict] = []
  148. for menu in descendants:
  149. if (menu.menu_type or "").upper() == "F":
  150. continue
  151. path = (menu.path or "").strip()
  152. if not path:
  153. continue
  154. if "form-sink" in path.lower() or "水槽" in str(menu.menu_name or ""):
  155. continue
  156. if allowed_ids is not None and menu.id not in allowed_ids:
  157. continue
  158. colors = _mobile_menu_icon_colors(path)
  159. parent = by_id.get((menu.parent_id or "").strip())
  160. item = {
  161. "id": menu.id,
  162. "title": menu.menu_name,
  163. "path": path if path.startswith("/") else f"/{path}",
  164. "icon": _mobile_menu_icon(path),
  165. "iconBg": colors["iconBg"],
  166. "iconFg": colors["iconFg"],
  167. "perms": (menu.perms or "").strip() or None,
  168. }
  169. if parent is not None and parent.id != root.id:
  170. item["groupTitle"] = parent.menu_name
  171. items.append(item)
  172. return items
  173. @router.get("/menus")
  174. async def list_my_mobile_menus(request: Request, db: Session = Depends(get_db)):
  175. try:
  176. user = await _current_user(request, db)
  177. except Exception:
  178. return error_response(401, "未授权")
  179. user_id = str(user.get("user_id") or user.get("sub") or "").strip()
  180. tenant_no = str(user.get("tenant_no") or "").strip()
  181. if not user_id or not tenant_no:
  182. return error_response(401, "未授权")
  183. web_role = resolve_web_role_for_user(db, user_id, tenant_no=tenant_no)
  184. items = list_mobile_menu_items(
  185. db,
  186. user_id=user_id,
  187. tenant_no=tenant_no,
  188. is_super_admin=web_role == "super_admin",
  189. )
  190. return success_response({"list": items})