فهرست منبع

补充提交分支剩余改动与生成产物。

包含后端/前端其余功能修改、文档与 graphify 输出更新,确保当前工作区变更与远端分支一致。

Made-with: Cursor
zlwu 6 ماه پیش
والد
کامیت
0023d40f61
100فایلهای تغییر یافته به همراه1837 افزوده شده و 241 حذف شده
  1. 2 2
      backend/app/api/menu.py
  2. 1 1
      backend/app/api/product_banner.py
  3. 1 1
      backend/app/api/product_detail_media.py
  4. 1 1
      backend/app/api/product_parameter_value.py
  5. 1 1
      backend/app/api/product_review.py
  6. 48 1
      backend/app/api/public.py
  7. 54 0
      backend/app/core/menu_guard.py
  8. 164 41
      backend/app/crud/statistics.py
  9. 5 0
      backend/app/crud/user.py
  10. 60 0
      backend/app/models/models.py
  11. 68 0
      backend/scripts/migration_20260429_user_scope_menu_code.sql
  12. 430 0
      docs/superpowers/plans/2026-04-29-web-admin-dashboard-console-region-tab-scope-implementation.md
  13. 173 0
      docs/superpowers/plans/2026-04-29-web-admin-super-admin-menu-region-implementation.md
  14. 108 0
      docs/superpowers/specs/2026-04-29-web-admin-dashboard-console-region-tab-and-scope-design.md
  15. 357 0
      docs/superpowers/specs/2026-04-29-web-admin-super-admin-menu-region-design.md
  16. BIN
      docs/superpowers/uat-home.png
  17. 146 0
      docs/superpowers/uat-role-region-menu-2026-04-29.json
  18. BIN
      docs/superpowers/uat-screens/debug-after-login.png
  19. BIN
      docs/superpowers/uat-screens/debug-login-page.png
  20. BIN
      docs/superpowers/uat-screens/multi-admin-error.png
  21. BIN
      docs/superpowers/uat-screens/super-admin-error.png
  22. 24 0
      docs/superpowers/uat-ui-role-check-2026-04-29.json
  23. 159 193
      graphify-out/GRAPH_REPORT.md
  24. 1 0
      graphify-out/cache/ast/03947cd05f5975be094042c91f56f0fe10d11f4fc5ae079608f9dda7e43d32da.json
  25. 0 0
      graphify-out/cache/ast/05f42f7c6f3b7da70a11fa57a554a514543711642e12e1b85abe619cc6df38cd.json
  26. 0 0
      graphify-out/cache/ast/064aae287ee25aa610bb95b26edfd2546415717022aa9e5f1e89304c8c012b90.json
  27. 1 0
      graphify-out/cache/ast/0836f9c9ec4fd76081af297798e5e5306ba79258fb9a8585fedcdbc2864afc11.json
  28. 1 0
      graphify-out/cache/ast/08dee9727ab48d9f67f97a96e2e99988c97efb6a77e6145a108263aa69f9d9eb.json
  29. 0 0
      graphify-out/cache/ast/0d3bcd0ab87819a29c3b830347a91cdf4b74d91bb70024db556c2ca9b74cf61f.json
  30. 1 0
      graphify-out/cache/ast/0fc0f2b3fe9ec32bf063772f01243e2f13d914a103c383d6234050b79dff993f.json
  31. 1 0
      graphify-out/cache/ast/104391934eedb1d272798974909ee1852ecc1c59736910b5df9fcafac78e349e.json
  32. 0 0
      graphify-out/cache/ast/1188b91360c1eda95fca805e895436e6ecf2e99b1f383d6439f421a51c65d3fe.json
  33. 0 0
      graphify-out/cache/ast/11b7d5bbb739da422c56eb5cea161d1d8669c8cbc017e05e9c4f743a62ca5d74.json
  34. 0 0
      graphify-out/cache/ast/199204831a214bd3c1997062427253a0445835bac801eb9a0cc426feab17f023.json
  35. 0 0
      graphify-out/cache/ast/1a4320188c67cca89f4df9a9cac739f92544e6611196a17c6c19707fefe8a008.json
  36. 0 0
      graphify-out/cache/ast/1ab3a4687e0a158d6cf2b43d9f630d744551fb59ee5845beca7086b44ab16be3.json
  37. 1 0
      graphify-out/cache/ast/1b77c8772d3757c001bd8d191515c66f939bef47fc8468d73ba2b904c515a323.json
  38. 1 0
      graphify-out/cache/ast/1bf8476c3a284f745f0c7f6ca0058a1a832600a4bc11a5ffcc513227acfa256d.json
  39. 0 0
      graphify-out/cache/ast/1da2174c6e477538709721aae17956b56d8c23865ca8abc2e0bc3869ba3fef49.json
  40. 0 0
      graphify-out/cache/ast/1eaeb2c94cd5780cbd5bd8b36341de1747147a5db72e6823a2cf7a00f58cf2f6.json
  41. 0 0
      graphify-out/cache/ast/1fe590f88dbf3300c5dd8884ea8a665a6092c405f1d4fe5f63496959594148c9.json
  42. 0 0
      graphify-out/cache/ast/204becf41bbbe95c690e7679fcc7446bb7dc06e40e0f74ad3a4e8ac06daad742.json
  43. 1 0
      graphify-out/cache/ast/2308aa152409b5985d16f30c90d27acd95c9dc918a8c66f58a5687388b1b742e.json
  44. 1 0
      graphify-out/cache/ast/25a1c0c0fdf91536606ecf77e5ded3e56c70920fda3927933cb517d0dbca2abb.json
  45. 1 0
      graphify-out/cache/ast/297ba8655b29b5bf2b70833e3b76c9fbe3fd69ba5e6d910a46a9ea199c6afdce.json
  46. 0 0
      graphify-out/cache/ast/29ce30448874089741acbe1248299a30031349334470eb9ab7900540ce678cac.json
  47. 0 0
      graphify-out/cache/ast/2e0fbcbccb2d6a78a15569a7b93c0d11bd6fb32cc2706b6de17553af053622b6.json
  48. 1 0
      graphify-out/cache/ast/2e213a8777799f206f600e762488136fbf7d19d636c3674d93889e8ccfa2f263.json
  49. 1 0
      graphify-out/cache/ast/2e73adc434e154903a4ab163212346e4ccf564a9114de18cb076ccfb91fb4218.json
  50. 1 0
      graphify-out/cache/ast/2ead12d1f8729f7428e3bde66e4c2c6319bd5ee18cc7269795b6ed3f88382f86.json
  51. 1 0
      graphify-out/cache/ast/300cfabaa2d74feb8bba359e0bb03832a3c5e01fa68076f0a6812e276cc703ba.json
  52. 0 0
      graphify-out/cache/ast/3098b1bcaa73be883ce6597f633d7cf40f7bea2f16f8451ee502e3a3a6f2a3a8.json
  53. 0 0
      graphify-out/cache/ast/33a32a68e0c3c3aa9d075743067fc4c87adfe496068b327661afb89d1e51a7fc.json
  54. 1 0
      graphify-out/cache/ast/33ae13cf79efefa6f01a364099f1237f42fa133cf3a4cc8b41faac11ddfff0c0.json
  55. 1 0
      graphify-out/cache/ast/38bae257b39464ef6bfa567d418da9fd26c7829a1ce74a61a13bba31c26f6bd0.json
  56. 0 0
      graphify-out/cache/ast/392feff2cd2311c81a95d30cdb370a53429f88d540ea0b95a4c6330a6c22d017.json
  57. 1 0
      graphify-out/cache/ast/3a65beebde0f3cc6b6ab18b63831bb25bb1a44f6cbae6641625c6506e1dc8f6e.json
  58. 0 0
      graphify-out/cache/ast/3b4a9fc2b951424b2ea706d12cb54595ea10c469fadf8a1f69a6eb4cb12c8527.json
  59. 0 0
      graphify-out/cache/ast/3d3a445e2c77750e7f13827e6979e193deb731960a81b6f615f169607cb2e36b.json
  60. 0 0
      graphify-out/cache/ast/3d826ad9e6fb66ea4ecfe8ea7fb853be5bbb60f446d2eb1c02ba0bcee9f12def.json
  61. 0 0
      graphify-out/cache/ast/3e11fd520a3a26c0c4a2dd536633819bc5890b492a17edb1639aae60a41d0ec4.json
  62. 1 0
      graphify-out/cache/ast/40f833a6e47b6ec2a35bfb6af468891c95d221fcf547dc0aa169087d83d492f0.json
  63. 1 0
      graphify-out/cache/ast/418a69a836e1bfb851db54a5cc13ce9930bc67696cad681232d69aa8a78343f1.json
  64. 1 0
      graphify-out/cache/ast/41b1cb9bb73231c8bacfacd6ab60997e288886a21a53525fee652123d93edf77.json
  65. 0 0
      graphify-out/cache/ast/429d76cf8147213dda2aad7d4981f9d318c28c83bf24246fecdc5b325f37e00d.json
  66. 1 0
      graphify-out/cache/ast/434a7cfb1a6720c6a0f9becd3cc8dc5e0d538cde508ad12ba737241cd729cfef.json
  67. 0 0
      graphify-out/cache/ast/438644b3807df88a472822ca6a8703dd67f670d53a074e7201231b20edebf4d9.json
  68. 0 0
      graphify-out/cache/ast/446418d6a7149f3ea7381a032965960d707b2a8e803abbae2bd0559b34150849.json
  69. 1 0
      graphify-out/cache/ast/463d240afaa41a831b6e42301d0c35dc74bc41937627e5ec0f76025210a85485.json
  70. 0 0
      graphify-out/cache/ast/46c7ee7cfaae9757f71317d2e73eaf6cea5f32ccf8b1cf4da2f6723f3c8be70d.json
  71. 0 0
      graphify-out/cache/ast/499dfe060c05d8115b40aa4e6e5850116b8cae496ddaf1c1353a14ad5a9f7ef0.json
  72. 0 0
      graphify-out/cache/ast/55aab4c3dc11ea97004d32b3a374f62c4d6ff8b760e5ae4da65d070d679d1f57.json
  73. 0 0
      graphify-out/cache/ast/55c5a5e4a7569058ee226b6aabefc816bd4d00e2eb14c35206c445d71f4540ec.json
  74. 0 0
      graphify-out/cache/ast/5747e6ea6da5a17d1e9a9b5c83036c610d252820d371f6f2d74f3e8ed503b4df.json
  75. 1 0
      graphify-out/cache/ast/5880f90d93cd0833e2638431430eff436677c71b34d188284395ff544c992d63.json
  76. 0 0
      graphify-out/cache/ast/58d2c3f9266a050dbba6d3563dc16944cd8c85cdcfe6b7ef46d55080db885a7a.json
  77. 1 0
      graphify-out/cache/ast/594c6571c4a873c4e548b000b9bbcb1d5680e840052f9deaac4aac0849aabe27.json
  78. 0 0
      graphify-out/cache/ast/5b8be6a281267711bfe02769e371b7042cd6d9a6248fd50ca8b0f97f61c1d090.json
  79. 1 0
      graphify-out/cache/ast/65ffda25db0aae9c9fd644bc248e9c422b83c716a030c1e22349ffbb50191424.json
  80. 1 0
      graphify-out/cache/ast/68500d939d59801336cbf5078ddae2fcdb6821948720d6df768a43ba997d14fd.json
  81. 0 0
      graphify-out/cache/ast/6a24101e065e26af0d09fad5ddb85a060db628903c8f84b13ad700363dc1904f.json
  82. 1 0
      graphify-out/cache/ast/6a34e0fd99b3e805cf04416e43fba0c4c4c41ecd31243ad22435cc8188d26473.json
  83. 1 0
      graphify-out/cache/ast/6b43dffcf38c4f82156febc9f7ef3950aa767ef112b32109a15c291dfdf187c1.json
  84. 1 0
      graphify-out/cache/ast/6ee93c87fa8ba7de226bd0f1ce149beef9bba8bafca290e5c929ff20a000c661.json
  85. 1 0
      graphify-out/cache/ast/701d99450dd7b094a81ec80c175f45f8b5c9b93781f6c74d74ec160eff7f9bee.json
  86. 1 0
      graphify-out/cache/ast/717cf52372bb1442df1f5fc48ac47c818dc25d8e9ebbb7d1b2c5038d5cb2ffc5.json
  87. 0 0
      graphify-out/cache/ast/722a5ceaa006d1a13a755ffe8b3a00d6dab791f715003d6f097bd9134eafe5ca.json
  88. 0 0
      graphify-out/cache/ast/72de360d5f68a4930b7ce37be3ccdfa01550b8c40924506fc6a66a97b5623157.json
  89. 0 0
      graphify-out/cache/ast/7308712a83c4f2141f72190a5d7df1b1038b237ac046c0a4559ded5349720b24.json
  90. 0 0
      graphify-out/cache/ast/737e043d54f751035ae46cdccd41e37f405a523f8eafbb49cf3b7f3e98721d2d.json
  91. 0 0
      graphify-out/cache/ast/754b8c35f364b3c7dce48e7c92e968b261d19c9613a1323e2fc94bf6bf37a97d.json
  92. 1 0
      graphify-out/cache/ast/75b7dde9a3ec52a106864d9199170d0581b9c35271f81d332e730020649d9fe9.json
  93. 0 0
      graphify-out/cache/ast/75dea6c8de67695fa2c48f81247f7fa77d65352ae6b1b717f71c48248312ff85.json
  94. 0 0
      graphify-out/cache/ast/76bbb5556baec19033c196c6453c5f4f1ed22b8b4bb6ce122d814f550fde3bb0.json
  95. 0 0
      graphify-out/cache/ast/78c2314b5785565c880737cf199f00e8a03d19a2f55b598fe308bf222424f73a.json
  96. 0 0
      graphify-out/cache/ast/7af904771af355220cc7065ee03ecdc865d86386c4b04f1e74f2de2b1245ce9a.json
  97. 1 0
      graphify-out/cache/ast/7b1b7d1b58b862ef2457958888a0a46a18b19e43c5c1465b0ceb8faa0824c2b5.json
  98. 1 0
      graphify-out/cache/ast/7b2d775c8110b15a060457a318f01b54d7df90933b3d388e0cde73440e3c3b13.json
  99. 1 0
      graphify-out/cache/ast/7bad736e3e79bb428518a9bcdf4e28266867dac6cc113e504793bc6e950bcc2f.json
  100. 0 0
      graphify-out/cache/ast/7e6109fc138e14ba2354c1a58df48d07c7f76bca4fde8fcdceb28149ac6cd665.json

+ 2 - 2
backend/app/api/menu.py

@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
 from sqlalchemy import asc
 
 from app.core.database import get_db
-from app.core.auth import get_current_user
+from app.core.menu_guard import require_admin_or_super_admin
 from app.core.response import success_response
 from app.models.models import SysMenu
 
@@ -61,7 +61,7 @@ def build_menu_tree(menus: list[SysMenu]) -> list[dict]:
 @router.get("/list")
 async def get_menu_list(
     db: Session = Depends(get_db),
-    current_user: dict = Depends(get_current_user)
+    current_user: dict = Depends(require_admin_or_super_admin)
 ):
     """
     获取菜单列表(从 sys_menu 表读取,构建树形结构)

+ 1 - 1
backend/app/api/product_banner.py

@@ -21,7 +21,7 @@ router = APIRouter(prefix="/web/admin/product-banners", tags=["商品轮播图
 
 def require_admin(current_user: dict = Depends(get_current_user)):
     """验证运营人员权限"""
-    if current_user.get("role") != "admin":
+    if current_user.get("role") not in {"admin", "super_admin"}:
         raise HTTPException(status_code=403, detail="需要运营人员权限")
     return current_user
 

+ 1 - 1
backend/app/api/product_detail_media.py

@@ -21,7 +21,7 @@ router = APIRouter(prefix="/web/admin/product-detail-media", tags=["商品详情
 
 def require_admin(current_user: dict = Depends(get_current_user)):
     """验证运营人员权限"""
-    if current_user.get("role") != "admin":
+    if current_user.get("role") not in {"admin", "super_admin"}:
         raise HTTPException(status_code=403, detail="需要运营人员权限")
     return current_user
 

+ 1 - 1
backend/app/api/product_parameter_value.py

@@ -19,7 +19,7 @@ router = APIRouter(prefix="/web/admin/product-parameter-values", tags=["商品
 
 def require_admin(current_user: dict = Depends(get_current_user)):
     """验证运营人员权限"""
-    if current_user.get("role") != "admin":
+    if current_user.get("role") not in {"admin", "super_admin"}:
         raise HTTPException(status_code=403, detail="需要运营人员权限")
     return current_user
 

+ 1 - 1
backend/app/api/product_review.py

@@ -23,7 +23,7 @@ router = APIRouter(prefix="/web/admin/reviews", tags=["售后评价管理"])
 
 def require_admin(current_user: dict = Depends(get_current_user)):
     """验证运营人员权限"""
-    if current_user.get("role") != "admin":
+    if current_user.get("role") not in {"admin", "super_admin"}:
         raise HTTPException(status_code=403, detail="需要运营人员权限")
     return current_user
 

+ 48 - 1
backend/app/api/public.py

@@ -28,7 +28,7 @@ from app.core.auth import (
 )
 from app.core.config import settings
 from app.core.response import success_response, error_response
-from app.models.models import SysUser
+from app.models.models import SysUser, SysUserMenuScope, SysUserRegionScope
 from app.crud.user import user_crud
 from app.crud.region import region_crud
 from app.crud.password_history import password_history_crud
@@ -264,6 +264,15 @@ async def get_current_user(request: Request, db: Session = Depends(get_db)):
         if current_user.get("region_id"):
             region = region_crud.get(db, current_user.get("region_id"))
             region_name = region.name if region else None
+        region_scope_rows = (
+            db.query(SysUserRegionScope.region_id)
+            .filter(
+                SysUserRegionScope.user_id == current_user.get("user_id"),
+                SysUserRegionScope.is_del == 0,
+            )
+            .all()
+        )
+        region_ids = sorted({str(row[0]).strip() for row in region_scope_rows if row and row[0]})
 
         au = current_user.get("avatar_url")
         avatar_url = str(au).strip() if au else None
@@ -273,8 +282,10 @@ async def get_current_user(request: Request, db: Session = Depends(get_db)):
             "realName": current_user.get("real_name"),
             "phone": current_user.get("phone"),
             "role": current_user.get("role"),
+            "isSuperAdmin": current_user.get("role") == "super_admin",
             "regionId": current_user.get("region_id"),
             "regionName": region_name,
+            "regionIds": region_ids,
             "isActive": current_user.get("is_active"),
             "avatarUrl": avatar_url if avatar_url else None,
         })
@@ -282,6 +293,42 @@ async def get_current_user(request: Request, db: Session = Depends(get_db)):
         return error_response(401, "Token无效或已过期")
 
 
+@router.get("/web/auth/menu-codes", response_model=dict)
+async def get_web_auth_menu_codes(request: Request, db: Session = Depends(get_db)):
+    """获取当前用户可见菜单 code 列表。super_admin 永远全菜单。"""
+    authorization = request.headers.get("Authorization", "")
+    if not authorization.startswith("Bearer "):
+        return error_response(401, "未授权")
+
+    token = authorization.replace("Bearer ", "")
+    try:
+        current_user = await get_current_user_by_token(token=token, db=db)
+        role = current_user.get("role")
+        if role == "super_admin":
+            return success_response({
+                "isSuperAdmin": True,
+                "all": True,
+                "menuCodes": []
+            })
+
+        rows = (
+            db.query(SysUserMenuScope.menu_code)
+            .filter(
+                SysUserMenuScope.user_id == current_user.get("user_id"),
+                SysUserMenuScope.is_del == 0,
+            )
+            .all()
+        )
+        menu_codes = sorted({str(row[0]).strip() for row in rows if row and row[0]})
+        return success_response({
+            "isSuperAdmin": False,
+            "all": False,
+            "menuCodes": menu_codes
+        })
+    except HTTPException:
+        return error_response(401, "Token无效或已过期")
+
+
 # ============ 小程序端接口 ============
 
 

+ 54 - 0
backend/app/core/menu_guard.py

@@ -0,0 +1,54 @@
+"""菜单 code 权限守卫"""
+from typing import Callable, Set
+
+from fastapi import Depends, HTTPException
+from sqlalchemy.orm import Session
+
+from app.core.auth import get_current_user
+from app.core.database import get_db
+from app.models.models import SysUserMenuScope
+
+ADMIN_ROLES = {"admin", "super_admin"}
+SUPER_ADMIN_ROLE = "super_admin"
+
+
+def require_admin_or_super_admin(current_user: dict = Depends(get_current_user)):
+    """运营端角色守卫:admin / super_admin 可访问。"""
+    role = current_user.get("role")
+    if role not in ADMIN_ROLES:
+        raise HTTPException(status_code=403, detail="需要运营人员权限")
+    return current_user
+
+
+def get_user_menu_codes(db: Session, user_id: str) -> Set[str]:
+    """读取用户授权的菜单 code 集合。"""
+    if not user_id:
+        return set()
+    rows = (
+        db.query(SysUserMenuScope.menu_code)
+        .filter(
+            SysUserMenuScope.user_id == user_id,
+            SysUserMenuScope.is_del == 0,
+        )
+        .all()
+    )
+    return {str(row[0]).strip() for row in rows if row and row[0]}
+
+
+def require_menu_code(menu_code: str) -> Callable:
+    """接口菜单权限守卫。super_admin 直接放行。"""
+    normalized = (menu_code or "").strip()
+
+    def _dep(
+        current_user: dict = Depends(require_admin_or_super_admin),
+        db: Session = Depends(get_db),
+    ):
+        role = current_user.get("role")
+        if role == SUPER_ADMIN_ROLE:
+            return current_user
+        codes = get_user_menu_codes(db, current_user.get("user_id"))
+        if normalized not in codes:
+            raise HTTPException(status_code=403, detail="无菜单访问权限")
+        return current_user
+
+    return _dep

+ 164 - 41
backend/app/crud/statistics.py

@@ -73,21 +73,29 @@ class StatisticsCRUD:
     def get_order_trend(
         db: Session,
         region_id: str = None,
-        days: int = 7
+        days: int = 7,
+        include_inactive_regions: bool = False,
     ) -> List[Dict[str, Any]]:
         """获取订单趋势数据(按日统计)"""
         result = []
         today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+        region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
 
         for i in range(days - 1, -1, -1):
             day_start = today - timedelta(days=i)
             day_end = day_start + timedelta(days=1)
 
-            query = db.query(BizOrder).filter(
-                and_(
-                    BizOrder.is_del == 0,
-                    BizOrder.create_at >= day_start,
-                    BizOrder.create_at < day_end
+            query = (
+                db.query(BizOrder)
+                .join(SysRegion, BizOrder.region_id == SysRegion.id)
+                .filter(
+                    and_(
+                        BizOrder.is_del == 0,
+                        BizOrder.create_at >= day_start,
+                        BizOrder.create_at < day_end,
+                        SysRegion.is_del == 0,
+                    *region_active_clause,
+                    )
                 )
             )
 
@@ -110,14 +118,15 @@ class StatisticsCRUD:
     def get_today_order_stats_by_region(
         db: Session,
         region_id: str = None,
+        include_inactive_regions: bool = False,
     ) -> List[Dict[str, Any]]:
-        """今日各区域营业额(元)、订单笔数;列出未删除且启用的区域,无单则为 0。"""
+        """今日各区域营业额(元)、订单笔数;列出未删除的区域,无单则为 0。"""
         today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
         day_end = today + timedelta(days=1)
+        region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
 
         reg_q = db.query(SysRegion).filter(
             SysRegion.is_del == 0,
-            SysRegion.is_active == True,
         )
         if region_id:
             reg_q = reg_q.filter(SysRegion.id == region_id)
@@ -129,10 +138,13 @@ class StatisticsCRUD:
                 func.count(BizOrder.id).label("order_count"),
                 func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
             )
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
             .filter(
                 BizOrder.is_del == 0,
                 BizOrder.create_at >= today,
                 BizOrder.create_at < day_end,
+                SysRegion.is_del == 0,
+                *region_active_clause,
             )
             .group_by(BizOrder.region_id)
         )
@@ -158,20 +170,25 @@ class StatisticsCRUD:
         db: Session,
         region_id: str = None,
         limit: int = 30,
+        include_inactive_regions: bool = False,
     ) -> List[Dict[str, Any]]:
         """今日各下单客户:下单金额合计(元)、商品数量合计(订单行数量之和);按金额降序,最多 limit 条。"""
         today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
         day_end = today + timedelta(days=1)
+        region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
 
         amt_q = (
             db.query(
                 BizOrder.client_id,
                 func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
             )
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
             .filter(
                 BizOrder.is_del == 0,
                 BizOrder.create_at >= today,
                 BizOrder.create_at < day_end,
+                SysRegion.is_del == 0,
+                *region_active_clause,
             )
         )
         if region_id:
@@ -193,7 +210,9 @@ class StatisticsCRUD:
                     BizOrder.create_at < day_end,
                 ),
             )
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
             .filter(BizOrderItem.is_del == 0)
+            .filter(SysRegion.is_del == 0, *region_active_clause)
         )
         if region_id:
             qty_q = qty_q.filter(BizOrder.region_id == region_id)
@@ -513,7 +532,8 @@ class StatisticsCRUD:
     @staticmethod
     def get_dashboard_summary(
         db: Session,
-        region_id: str = None
+        region_id: str = None,
+        include_inactive_regions: bool = False,
     ) -> Dict[str, Any]:
         """获取仪表盘汇总数据(运营工作台;含待办摘要、最新订单、预警数等,字段驼峰)"""
         today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
@@ -521,9 +541,20 @@ class StatisticsCRUD:
         month_start = today.replace(day=1)
         yesterday_start = today - timedelta(days=1)
         region_clause = StatisticsCRUD._dashboard_order_region_clause(region_id)
+        region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
 
-        order_query = db.query(BizOrder).filter(
-            and_(BizOrder.is_del == 0, BizOrder.create_at >= month_start, *region_clause)
+        order_query = (
+            db.query(BizOrder)
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
+            .filter(
+                and_(
+                    BizOrder.is_del == 0,
+                    BizOrder.create_at >= month_start,
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
+                    *region_clause,
+                )
+            )
         )
         month_orders = order_query.all()
         today_orders = [
@@ -535,11 +566,17 @@ class StatisticsCRUD:
             if o.create_at and yesterday_start <= o.create_at < today
         ]
 
-        pending_delivery_q = db.query(BizOrder).filter(
-            and_(
-                BizOrder.is_del == 0,
-                BizOrder.delivery_status == "pending_dispatch",
-                *region_clause,
+        pending_delivery_q = (
+            db.query(BizOrder)
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
+            .filter(
+                and_(
+                    BizOrder.is_del == 0,
+                    BizOrder.delivery_status == "pending_dispatch",
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
+                    *region_clause,
+                )
             )
         )
         pending_delivery_count = pending_delivery_q.count()
@@ -566,28 +603,43 @@ class StatisticsCRUD:
         prev_amount = round(sum(float(o.total_amount or 0) for o in prev_day_orders), 2)
 
         # 未解决的配送时效预警(可按区域通过任务表关联)
-        warn_q = db.query(BizDeliveryWarning).filter(
-            and_(BizDeliveryWarning.is_del == 0, BizDeliveryWarning.is_resolved == 0)
-        )
-        if region_id:
-            warn_q = warn_q.join(
+        warn_q = (
+            db.query(BizDeliveryWarning)
+            .join(
                 BizDeliveryTask,
                 and_(
                     BizDeliveryTask.id == BizDeliveryWarning.task_id,
                     BizDeliveryTask.is_del == 0,
-                    BizDeliveryTask.region_id == region_id,
                 ),
             )
+            .join(SysRegion, BizDeliveryTask.region_id == SysRegion.id)
+            .filter(
+                and_(
+                    BizDeliveryWarning.is_del == 0,
+                    BizDeliveryWarning.is_resolved == 0,
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
+                )
+            )
+        )
+        if region_id:
+            warn_q = warn_q.filter(BizDeliveryTask.region_id == region_id)
         delivery_warning_count = warn_q.count()
 
         # 今日新提交报价:状态为已提交,且提交时间(无则用创建时间)落在当日 0 点~次日 0 点
         quot_submitted_at = func.coalesce(BizQuotation.submit_time, BizQuotation.create_at)
-        sub_quot_q = db.query(BizQuotation).filter(
-            and_(
-                BizQuotation.is_del == 0,
-                BizQuotation.status == "submitted",
-                quot_submitted_at >= today,
-                quot_submitted_at < tomorrow_start,
+        sub_quot_q = (
+            db.query(BizQuotation)
+            .join(SysRegion, BizQuotation.region_id == SysRegion.id)
+            .filter(
+                and_(
+                    BizQuotation.is_del == 0,
+                    BizQuotation.status == "submitted",
+                    quot_submitted_at >= today,
+                    quot_submitted_at < tomorrow_start,
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
+                )
             )
         )
         if region_id:
@@ -597,12 +649,15 @@ class StatisticsCRUD:
         sub_quot_preview = (
             db.query(BizQuotation, BizInquiry)
             .join(BizInquiry, BizInquiry.id == BizQuotation.inquiry_id)
+            .join(SysRegion, BizQuotation.region_id == SysRegion.id)
             .filter(
                 BizQuotation.is_del == 0,
                 BizQuotation.status == "submitted",
                 quot_submitted_at >= today,
                 quot_submitted_at < tomorrow_start,
                 BizInquiry.is_del == 0,
+                SysRegion.is_del == 0,
+                *region_active_clause,
                 *([BizQuotation.region_id == region_id] if region_id else []),
             )
             .order_by(desc(BizQuotation.submit_time), desc(BizQuotation.create_at))
@@ -616,10 +671,13 @@ class StatisticsCRUD:
 
         pd_preview = (
             db.query(BizOrder)
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
             .filter(
                 and_(
                     BizOrder.is_del == 0,
                     BizOrder.delivery_status == "pending_dispatch",
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
                     *region_clause,
                 )
             )
@@ -631,8 +689,15 @@ class StatisticsCRUD:
 
         cancel_preview = (
             db.query(BizOrder)
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
             .filter(
-                and_(BizOrder.is_del == 0, BizOrder.cancel_status == "pending_review", *region_clause)
+                and_(
+                    BizOrder.is_del == 0,
+                    BizOrder.cancel_status == "pending_review",
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
+                    *region_clause,
+                )
             )
             .order_by(desc(BizOrder.update_at))
             .limit(2)
@@ -654,12 +719,15 @@ class StatisticsCRUD:
 
         settle_preview = (
             db.query(BizOrder)
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
             .filter(
                 and_(
                     BizOrder.is_del == 0,
                     BizOrder.client_settlement_status == "pending",
                     BizOrder.pay_status == "paid",
                     BizOrder.order_status.in_(["pending_confirm", "completed"]),
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
                     *region_clause,
                 )
             )
@@ -683,12 +751,15 @@ class StatisticsCRUD:
 
         pending_settlement_count = (
             db.query(BizOrder)
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
             .filter(
                 and_(
                     BizOrder.is_del == 0,
                     BizOrder.client_settlement_status == "pending",
                     BizOrder.pay_status == "paid",
                     BizOrder.order_status.in_(["pending_confirm", "completed"]),
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
                     *region_clause,
                 )
             )
@@ -697,14 +768,24 @@ class StatisticsCRUD:
 
         pending_cancel_count = (
             db.query(BizOrder)
-            .filter(and_(BizOrder.is_del == 0, BizOrder.cancel_status == "pending_review", *region_clause))
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
+            .filter(
+                and_(
+                    BizOrder.is_del == 0,
+                    BizOrder.cancel_status == "pending_review",
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
+                    *region_clause,
+                )
+            )
             .count()
         )
 
         # 最新订单(工作台列表)
         recent_rows = (
             db.query(BizOrder)
-            .filter(and_(BizOrder.is_del == 0, *region_clause))
+            .join(SysRegion, BizOrder.region_id == SysRegion.id)
+            .filter(and_(BizOrder.is_del == 0, SysRegion.is_del == 0, *region_active_clause, *region_clause))
             .order_by(desc(BizOrder.create_at))
             .limit(8)
             .all()
@@ -713,7 +794,9 @@ class StatisticsCRUD:
         cid_set = list({o.client_id for o in recent_rows if o.client_id})
         region_name_map = {}
         if rid_set:
-            for r in db.query(SysRegion).filter(and_(SysRegion.is_del == 0, SysRegion.id.in_(rid_set))).all():
+            for r in db.query(SysRegion).filter(
+                and_(SysRegion.is_del == 0, *region_active_clause, SysRegion.id.in_(rid_set))
+            ).all():
                 region_name_map[r.id] = r.name
         client_name_map = {}
         if cid_set:
@@ -764,6 +847,39 @@ class StatisticsCRUD:
             },
         ]
 
+        # “客户总数/供应商总数”口径:统计用户表(sys_user)并按 region_id 区分
+        # - region_id 为空:不过滤全局
+        # - region_id 传入:按 SysUser.region_id 过滤
+        customer_count = (
+            db.query(SysUser)
+            .join(SysRegion, SysUser.region_id == SysRegion.id)
+            .filter(
+                and_(
+                    SysUser.is_del == 0,
+                    SysUser.role == "client",
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
+                    *([SysUser.region_id == region_id] if region_id else []),
+                )
+            )
+            .count()
+        )
+
+        supplier_count = (
+            db.query(SysUser)
+            .join(SysRegion, SysUser.region_id == SysRegion.id)
+            .filter(
+                and_(
+                    SysUser.is_del == 0,
+                    SysUser.role == "supplier",
+                    SysRegion.is_del == 0,
+                    *region_active_clause,
+                    *([SysUser.region_id == region_id] if region_id else []),
+                )
+            )
+            .count()
+        )
+
         return {
             "todayOrderCount": today_order_count,
             "todayOrderAmount": today_order_amount,
@@ -773,15 +889,22 @@ class StatisticsCRUD:
             "completedOrderCount": sum(
                 1 for o in month_orders if (getattr(o, "order_status", None) or o.status) == "completed"
             ),
-            "activePriceListCount": db.query(BizPriceList).filter(
-                and_(BizPriceList.is_del == 0, BizPriceList.status == "active")
-            ).count(),
-            "customerCount": db.query(SysUser).filter(
-                and_(SysUser.is_del == 0, SysUser.role == "client")
-            ).count(),
-            "supplierCount": db.query(SysUser).filter(
-                and_(SysUser.is_del == 0, SysUser.role == "supplier")
-            ).count(),
+            "activePriceListCount": (
+                db.query(BizPriceList)
+                .join(SysRegion, BizPriceList.region_id == SysRegion.id)
+                .filter(
+                    and_(
+                        BizPriceList.is_del == 0,
+                        BizPriceList.status == "active",
+                        SysRegion.is_del == 0,
+                        *region_active_clause,
+                        *([BizPriceList.region_id == region_id] if region_id else []),
+                    )
+                )
+                .count()
+            ),
+            "customerCount": customer_count,
+            "supplierCount": supplier_count,
             "todayOrderCountHint": _delta_hint(today_order_count, prev_count, " 单"),
             "todayOrderAmountHint": _amount_delta_hint(today_order_amount, prev_amount),
             "deliveryWarningCount": delivery_warning_count,

+ 5 - 0
backend/app/crud/user.py

@@ -51,6 +51,7 @@ class UserCRUD(CRUDBase[SysUser]):
         self,
         db: Session,
         role: str = None,
+        roles: Optional[List[str]] = None,
         region_id: str = None,
         keyword: str = None,
         is_active: bool = None,
@@ -62,6 +63,10 @@ class UserCRUD(CRUDBase[SysUser]):
         
         if role:
             query = query.filter(SysUser.role == role)
+        elif roles:
+            role_values = [str(x).strip() for x in roles if str(x).strip()]
+            if role_values:
+                query = query.filter(SysUser.role.in_(role_values))
         if region_id:
             query = query.filter(SysUser.region_id == region_id)
         if is_active is not None:

+ 60 - 0
backend/app/models/models.py

@@ -178,6 +178,66 @@ class SysMenu(Base):
     update_by = Column(String(32), nullable=True, comment="修改人ID")
 
 
+class SysUserRegionScope(Base):
+    """用户区域权限表"""
+    __tablename__ = "sys_user_region_scope"
+
+    id = Column(String(32), primary_key=True, comment="主键ID")
+    user_id = Column(String(32), nullable=False, comment="用户ID")
+    region_id = Column(String(32), nullable=False, comment="区域ID")
+    is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是")
+    create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+    create_by = Column(String(32), nullable=True, comment="创建人ID")
+    create_name = Column(String(100), nullable=True, comment="创建人姓名")
+    update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+    update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysUserRegionScopeHis(Base):
+    """用户区域权限历史表"""
+    __tablename__ = "sys_user_region_scope_his"
+
+    id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+    user_id = Column(String(32), nullable=False, comment="用户ID")
+    region_id = Column(String(32), nullable=False, comment="区域ID")
+    create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+    create_by = Column(String(32), nullable=True, comment="创建人ID")
+    create_name = Column(String(100), nullable=True, comment="创建人姓名")
+    update_at = Column(DateTime, nullable=True, comment="修改时间")
+    update_by = Column(String(32), nullable=True, comment="修改人ID")
+    his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class SysUserMenuScope(Base):
+    """用户菜单权限表(按菜单 code 授权)"""
+    __tablename__ = "sys_user_menu_scope"
+
+    id = Column(String(32), primary_key=True, comment="主键ID")
+    user_id = Column(String(32), nullable=False, comment="用户ID")
+    menu_code = Column(String(100), nullable=False, comment="菜单编码")
+    is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是")
+    create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+    create_by = Column(String(32), nullable=True, comment="创建人ID")
+    create_name = Column(String(100), nullable=True, comment="创建人姓名")
+    update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+    update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysUserMenuScopeHis(Base):
+    """用户菜单权限历史表"""
+    __tablename__ = "sys_user_menu_scope_his"
+
+    id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+    user_id = Column(String(32), nullable=False, comment="用户ID")
+    menu_code = Column(String(100), nullable=False, comment="菜单编码")
+    create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+    create_by = Column(String(32), nullable=True, comment="创建人ID")
+    create_name = Column(String(100), nullable=True, comment="创建人姓名")
+    update_at = Column(DateTime, nullable=True, comment="修改时间")
+    update_by = Column(String(32), nullable=True, comment="修改人ID")
+    his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
 class SysPasswordHistory(Base):
     """密码修改历史表"""
     __tablename__ = "sys_password_history"

+ 68 - 0
backend/scripts/migration_20260429_user_scope_menu_code.sql

@@ -0,0 +1,68 @@
+-- 运营端权限改造:super_admin + 多区域 + 菜单 code 授权
+
+-- 1) 角色迁移:admin -> super_admin
+UPDATE sys_user
+SET role = 'super_admin',
+    update_at = NOW()
+WHERE is_del = 0
+  AND role = 'admin';
+
+-- 2) 用户-区域权限表
+CREATE TABLE IF NOT EXISTS sys_user_region_scope (
+  id VARCHAR(32) NOT NULL COMMENT '主键ID',
+  user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
+  region_id VARCHAR(32) NOT NULL COMMENT '区域ID',
+  is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除',
+  create_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人ID',
+  create_name VARCHAR(100) DEFAULT NULL COMMENT '创建人姓名',
+  update_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  update_by VARCHAR(32) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_user_region_scope (user_id, region_id, is_del),
+  KEY idx_user_region_scope_user (user_id, is_del),
+  KEY idx_user_region_scope_region (region_id, is_del)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户区域权限';
+
+-- 3) 用户-区域权限历史表(无索引、无 is_del)
+CREATE TABLE IF NOT EXISTS sys_user_region_scope_his (
+  id VARCHAR(32) NOT NULL COMMENT '主键ID',
+  user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
+  region_id VARCHAR(32) NOT NULL COMMENT '区域ID',
+  create_at DATETIME DEFAULT NULL COMMENT '创建时间',
+  create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人ID',
+  create_name VARCHAR(100) DEFAULT NULL COMMENT '创建人姓名',
+  update_at DATETIME DEFAULT NULL COMMENT '修改时间',
+  update_by VARCHAR(32) DEFAULT NULL COMMENT '修改人ID',
+  his_create_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '历史记录创建时间'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户区域权限历史';
+
+-- 4) 用户-菜单 code 权限表
+CREATE TABLE IF NOT EXISTS sys_user_menu_scope (
+  id VARCHAR(32) NOT NULL COMMENT '主键ID',
+  user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
+  menu_code VARCHAR(100) NOT NULL COMMENT '菜单编码',
+  is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除',
+  create_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人ID',
+  create_name VARCHAR(100) DEFAULT NULL COMMENT '创建人姓名',
+  update_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  update_by VARCHAR(32) DEFAULT NULL COMMENT '修改人ID',
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_user_menu_scope (user_id, menu_code, is_del),
+  KEY idx_user_menu_scope_user (user_id, is_del),
+  KEY idx_user_menu_scope_code (menu_code, is_del)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户菜单权限';
+
+-- 5) 用户-菜单 code 权限历史表(无索引、无 is_del)
+CREATE TABLE IF NOT EXISTS sys_user_menu_scope_his (
+  id VARCHAR(32) NOT NULL COMMENT '主键ID',
+  user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
+  menu_code VARCHAR(100) NOT NULL COMMENT '菜单编码',
+  create_at DATETIME DEFAULT NULL COMMENT '创建时间',
+  create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人ID',
+  create_name VARCHAR(100) DEFAULT NULL COMMENT '创建人姓名',
+  update_at DATETIME DEFAULT NULL COMMENT '修改时间',
+  update_by VARCHAR(32) DEFAULT NULL COMMENT '修改人ID',
+  his_create_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '历史记录创建时间'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户菜单权限历史';

+ 430 - 0
docs/superpowers/plans/2026-04-29-web-admin-dashboard-console-region-tab-scope-implementation.md

@@ -0,0 +1,430 @@
+# Web Admin Dashboard Console Region Tab & Scope Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** In `web-admin` Console (`/dashboard/console`) add a top region tab bar and make all displayed statistics obey `super_admin` (all regions) vs `admin` (only permission scope, with “all” meaning sum of permitted regions). “Latest orders” are merged across regions and sorted by `createAt` descending.
+
+**Architecture:** Add region tab UI + tab state in `web-admin/src/views/dashboard/console/index.vue`. Refactor data loading into “scoped” loaders that either:
+- call dashboard APIs once when scope is a single region or `super_admin/all`, or
+- call APIs for each permitted region and aggregate results for `admin/all`.
+
+**Tech Stack:** Vue3 `<script setup lang="ts">`, Pinia `useUserStore`, Element Plus `el-button`, existing REST APIs in `web-admin/src/api/business.ts`, region list from `web-admin/src/api/admin.ts` (`fetchActiveRegions`).
+
+---
+
+## Task 1: Add Region Tabs UI + Tab State
+
+**Files:**
+- Modify: `web-admin/src/views/dashboard/console/index.vue`
+
+### Step 1: Add region tab bar in template
+
+Insert this block **immediately after** the existing `page-head` section (right after the closing `</div>` of `.page-head` and before the alert bar):
+
+```vue
+<div class="region-tabs region-tab-strip">
+  <div class="tab-button-group region-tab-group">
+    <el-button
+      class="region-tab-btn"
+      :type="activeRegionTabKey === 'all' ? 'primary' : 'default'"
+      @click="selectRegionTab('all')"
+    >
+      全部区域
+    </el-button>
+    <el-button
+      v-for="r in allowedRegionList"
+      :key="r.id"
+      class="region-tab-btn"
+      :type="activeRegionTabKey === r.id ? 'primary' : 'default'"
+      @click="selectRegionTab(r.id)"
+    >
+      {{ r.name }}
+    </el-button>
+  </div>
+</div>
+```
+
+### Step 2: Add missing UI styles (scoped)
+
+In `<style lang="scss" scoped>`, add under `.console-container { ... }`:
+
+```scss
+.tab-button-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin: 0 0 12px;
+}
+
+.region-tab-btn {
+  min-width: 96px;
+}
+```
+
+### Step 3: Add tab state + region permissions computation
+
+In `<script setup lang="ts">`, add these imports:
+
+```ts
+import { fetchActiveRegions } from '@/api/admin'
+import { useUserStore } from '@/store/modules/user'
+```
+
+Then add the following state after existing `dashboard` ref:
+
+```ts
+const userStore = useUserStore()
+
+const regionList = ref<any[]>([])
+const activeRegionTabKey = ref<string>('all')
+
+const userRole = computed(() => userStore.getUserInfo?.role)
+
+const allowedRegionIds = computed<string[]>(() => {
+  const info: any = userStore.getUserInfo
+  if (info?.role === 'super_admin') return []
+  const ids = Array.isArray(info?.regionIds) ? info.regionIds : info?.regionId ? [info.regionId] : []
+  return ids.map((x: any) => String(x))
+})
+
+const allowedRegionList = computed(() => {
+  if (userRole.value === 'super_admin') return regionList.value
+  const allow = new Set(allowedRegionIds.value)
+  return regionList.value.filter((r: any) => allow.has(String(r.id)))
+})
+```
+
+### Step 4: Add tab selection handler + region loader
+
+Add these functions near existing `loadDashboard`/`loadOrderTrend` functions:
+
+```ts
+const selectRegionTab = (key: string) => {
+  if (activeRegionTabKey.value === key) return
+  activeRegionTabKey.value = key
+  void loadConsoleData()
+}
+
+const loadRegions = async () => {
+  const res = await fetchActiveRegions()
+  regionList.value = Array.isArray(res) ? res : []
+}
+```
+
+### Step 5: Adjust `onMounted` entry
+
+Replace the current `onMounted(() => { ... load... })` with:
+
+```ts
+onMounted(() => {
+  void loadRegions().then(() => loadConsoleData())
+})
+```
+
+---
+
+## Task 2: Implement Scoped Data Loaders (single region vs all)
+
+**Files:**
+- Modify: `web-admin/src/views/dashboard/console/index.vue`
+
+### Step 1: Add scope helpers
+
+Add the following helper functions in `<script setup>`:
+
+```ts
+const isSuperAdmin = computed(() => userRole.value === 'super_admin')
+
+const getSingleRegionId = () => {
+  return activeRegionTabKey.value !== 'all' ? String(activeRegionTabKey.value) : undefined
+}
+
+const getMultiRegionIds = () => {
+  return allowedRegionIds.value
+}
+```
+
+### Step 2: Refactor into one “loadConsoleData” orchestration
+
+Create a top-level loader:
+
+```ts
+const loadConsoleData = async () => {
+  loading.value = true
+  try {
+    await Promise.all([
+      loadDashboardScoped(),
+      loadOrderTrendScoped(),
+      loadTodayRegionStatsScoped(),
+      loadTodayClientStatsScoped()
+    ])
+  } finally {
+    loading.value = false
+  }
+}
+```
+
+### Step 3: Scoped dashboard loader with admin/all aggregation
+
+Replace existing `loadDashboard()` with `loadDashboardScoped()`:
+
+```ts
+const loadDashboardScoped = async () => {
+  const singleRegionId = getSingleRegionId()
+
+  // super_admin + all -> keep existing unscoped behavior
+  if (isSuperAdmin.value && !singleRegionId) {
+    dashboard.value = await fetchWorkbenchDashboard()
+    return
+  }
+
+  // single region
+  if (singleRegionId) {
+    dashboard.value = await fetchWorkbenchDashboard({ regionId: singleRegionId })
+    return
+  }
+
+  // admin + all -> aggregate over permission regions
+  const regionIds = getMultiRegionIds()
+  const results = await Promise.allSettled(regionIds.map((id) => fetchWorkbenchDashboard({ regionId: id })))
+  const ok = results
+    .filter((r): r is PromiseFulfilledResult<WorkbenchDashboardData> => r.status === 'fulfilled')
+    .map((r) => r.value)
+
+  const first = ok[0]
+  const sum = (key: keyof WorkbenchDashboardData) =>
+    ok.reduce((acc, d) => acc + Number((d as any)?.[key] ?? 0), 0)
+
+  const mergedRecentOrders = ok.flatMap((d) => (Array.isArray(d.recentOrders) ? d.recentOrders : []))
+  mergedRecentOrders.sort((a: any, b: any) => new Date(b.createAt || 0).getTime() - new Date(a.createAt || 0).getTime())
+
+  const snapMap = new Map<string, { type: string; title: string; count: number; items: string[] }>()
+  ok.forEach((d) => {
+    ;(d.todoSnapshots || []).forEach((s) => {
+      const k = String(s.type || s.title || '')
+      if (!k) return
+      const prev = snapMap.get(k)
+      if (!prev) {
+        snapMap.set(k, { type: s.type, title: s.title, count: Number(s.count ?? 0), items: Array.isArray(s.items) ? [...s.items] : [] })
+      } else {
+        prev.count += Number(s.count ?? 0)
+        const merged = [...prev.items, ...(Array.isArray(s.items) ? s.items : [])]
+        prev.items = Array.from(new Set(merged))
+      }
+    })
+  })
+
+  dashboard.value = {
+    todayOrderCount: sum('todayOrderCount'),
+    todayOrderAmount: sum('todayOrderAmount'),
+    monthOrderCount: sum('monthOrderCount'),
+    monthOrderAmount: sum('monthOrderAmount'),
+    pendingDeliveryCount: sum('pendingDeliveryCount'),
+    completedOrderCount: sum('completedOrderCount'),
+    activePriceListCount: sum('activePriceListCount'),
+    customerCount: sum('customerCount'),
+    supplierCount: sum('supplierCount'),
+    deliveryWarningCount: sum('deliveryWarningCount'),
+    todayOrderCountHint: first?.todayOrderCountHint,
+    todayOrderAmountHint: first?.todayOrderAmountHint,
+    recentOrders: mergedRecentOrders,
+    todoSnapshots: Array.from(snapMap.values())
+  } as WorkbenchDashboardData
+}
+```
+
+### Step 4: Scoped order trend aggregation
+
+Replace existing `loadOrderTrend()` with `loadOrderTrendScoped()`:
+
+```ts
+const loadOrderTrendScoped = async () => {
+  trendLoading.value = true
+  try {
+    const singleRegionId = getSingleRegionId()
+
+    // super_admin + all -> unscoped
+    if (isSuperAdmin.value && !singleRegionId) {
+      const list = await fetchOrderTrend({ days: TREND_DAYS })
+      orderTrend.value = Array.isArray(list) ? list : []
+      return
+    }
+
+    // single region
+    const regionIds = singleRegionId ? [singleRegionId] : getMultiRegionIds()
+    const results = await Promise.allSettled(regionIds.map((id) => fetchOrderTrend({ regionId: id, days: TREND_DAYS })))
+    const ok = results
+      .filter((r): r is PromiseFulfilledResult<OrderTrendDayItem[]> => r.status === 'fulfilled')
+      .map((r) => r.value)
+
+    const base = ok.find((x) => Array.isArray(x) && x.length > 0) || []
+    const baseDates = base.map((x) => x.date)
+
+    const m = new Map<string, OrderTrendDayItem>()
+    ok.forEach((list) => {
+      ;(list || []).forEach((row) => {
+        const k = String(row.date)
+        const prev = m.get(k)
+        if (!prev) {
+          m.set(k, { date: row.date, orderCount: Number(row.orderCount ?? 0), orderAmount: Number(row.orderAmount ?? 0) })
+        } else {
+          prev.orderCount += Number(row.orderCount ?? 0)
+          prev.orderAmount += Number(row.orderAmount ?? 0)
+        }
+      })
+    })
+
+    orderTrend.value = baseDates.map((d) => m.get(d)).filter(Boolean)
+  } catch {
+    orderTrend.value = []
+  } finally {
+    trendLoading.value = false
+  }
+}
+```
+
+### Step 5: Scoped “today by region” aggregation
+
+Replace existing `loadTodayRegionStats()` with `loadTodayRegionStatsScoped()`:
+
+```ts
+const loadTodayRegionStatsScoped = async () => {
+  regionTodayLoading.value = true
+  try {
+    const singleRegionId = getSingleRegionId()
+
+    // super_admin + all -> unscoped
+    if (isSuperAdmin.value && !singleRegionId) {
+      const list = await fetchTodayOrdersByRegion()
+      regionTodayList.value = Array.isArray(list) ? list : []
+      return
+    }
+
+    const regionIds = singleRegionId ? [singleRegionId] : getMultiRegionIds()
+    const results = await Promise.allSettled(regionIds.map((id) => fetchTodayOrdersByRegion({ regionId: id })))
+    const ok = results
+      .filter((r): r is PromiseFulfilledResult<any[]> => r.status === 'fulfilled')
+      .map((r) => r.value)
+    const flat = ok.flat()
+
+    const m = new Map<string, TodayRegionOrderStatItem>()
+    flat.forEach((it: any) => {
+      const rid = String(it.regionId)
+      const prev = m.get(rid)
+      if (!prev) {
+        m.set(rid, {
+          regionId: rid,
+          regionName: it.regionName || '',
+          orderCount: Number(it.orderCount ?? 0),
+          orderAmount: Number(it.orderAmount ?? 0)
+        })
+      } else {
+        prev.orderCount += Number(it.orderCount ?? 0)
+        prev.orderAmount += Number(it.orderAmount ?? 0)
+      }
+    })
+
+    const orderIds = singleRegionId ? [singleRegionId] : getMultiRegionIds()
+    regionTodayList.value = orderIds.map((id) => m.get(String(id))).filter(Boolean)
+  } catch {
+    regionTodayList.value = []
+  } finally {
+    regionTodayLoading.value = false
+  }
+}
+```
+
+### Step 6: Scoped “today by client” aggregation
+
+Replace existing `loadTodayClientStats()` with `loadTodayClientStatsScoped()`:
+
+```ts
+const loadTodayClientStatsScoped = async () => {
+  clientTodayLoading.value = true
+  try {
+    const singleRegionId = getSingleRegionId()
+
+    // super_admin + all -> unscoped
+    if (isSuperAdmin.value && !singleRegionId) {
+      const list = await fetchTodayOrdersByClient({ limit: 30 })
+      clientTodayList.value = Array.isArray(list) ? list : []
+      return
+    }
+
+    const regionIds = singleRegionId ? [singleRegionId] : getMultiRegionIds()
+    const limit = 30
+    const results = await Promise.allSettled(regionIds.map((id) => fetchTodayOrdersByClient({ regionId: id, limit })))
+    const ok = results
+      .filter((r): r is PromiseFulfilledResult<any[]> => r.status === 'fulfilled')
+      .map((r) => r.value)
+    const flat = ok.flat()
+
+    const m = new Map<string, TodayClientOrderStatItem>()
+    flat.forEach((it: any) => {
+      const cid = String(it.clientId)
+      const prev = m.get(cid)
+      if (!prev) {
+        m.set(cid, {
+          clientId: cid,
+          clientName: it.clientName || '',
+          orderAmount: Number(it.orderAmount ?? 0),
+          productQuantity: Number(it.productQuantity ?? 0)
+        })
+      } else {
+        prev.orderAmount += Number(it.orderAmount ?? 0)
+        prev.productQuantity += Number(it.productQuantity ?? 0)
+      }
+    })
+
+    const merged = Array.from(m.values())
+    merged.sort((a, b) => (Number(b.orderAmount) || 0) - (Number(a.orderAmount) || 0))
+    clientTodayList.value = merged
+  } catch {
+    clientTodayList.value = []
+  } finally {
+    clientTodayLoading.value = false
+  }
+}
+```
+
+---
+
+## Task 3: Ensure “Latest Orders” uses merged + createAt desc
+
+**Files:**
+- Modify: `web-admin/src/views/dashboard/console/index.vue`
+
+### Step 1: Remove any implicit assumptions
+
+Verify that the computed `orderList` remains:
+
+```ts
+const orderList = computed(() => dashboard.value?.recentOrders ?? [])
+```
+
+And ensure `recentOrders` is populated by `loadDashboardScoped()` aggregation, sorted by `createAt` descending:
+
+```ts
+mergedRecentOrders.sort((a: any, b: any) => new Date(b.createAt || 0).getTime() - new Date(a.createAt || 0).getTime())
+```
+
+No additional slicing or “top N” rule is applied beyond merging and sorting.
+
+---
+
+## Task 4: Manual Verification
+
+**Files:** none
+
+### Step 1: Super admin
+- [ ] 打开 `http://localhost:3006/#/dashboard/console`
+- [ ] 顶部出现区域 Tab:`全部区域` + 每个启用区域
+- [ ] 切换任一单区 Tab,所有面板(概览卡片/趋势/今日各区域/今日下单客户/最新订单)应只显示该区数据
+- [ ] 回到 `全部区域`,所有统计回到全量数据
+
+### Step 2: 运营(admin)多区域
+- [ ] 切到一个具有多个 region 权限的运营账号
+- [ ] 顶部 Tab 仅展示其权限范围区域 + `全部区域`
+- [ ] 切到权限某单区:展示该单区数据
+- [ ] 切到 `全部区域`:所有统计应等于“各权限区数据求和/合并”(最新订单按 `createAt` 倒序合并)
+

+ 173 - 0
docs/superpowers/plans/2026-04-29-web-admin-super-admin-menu-region-implementation.md

@@ -0,0 +1,173 @@
+# Web-Admin 超级管理员与菜单/区域权限 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在保持前端静态菜单模式前提下,实现 `super_admin` + 运营管理人员(`admin`) 的菜单 code 授权和多区域数据权限,并完成迁移与联调。
+
+**Architecture:** 后端新增“用户-区域范围”和“用户-菜单 code”关系表;登录后前端拉取授权 code 过滤本地静态菜单;接口侧仅保留角色与区域范围守卫。
+
+**Tech Stack:** FastAPI + SQLAlchemy + Vue3 + Pinia + Element Plus + MySQL
+
+---
+
+### Task 1: 数据库迁移脚本落地
+
+**Files:**
+- Create: `backend/scripts/migration_20260429_user_scope_menu_code.sql`
+- Modify: `docs/superpowers/specs/2026-04-29-web-admin-super-admin-menu-region-design.md`
+- Test: `docs/sql/sanqiang.sql` (对照结构)
+
+- [ ] **Step 1: 编写新表 DDL**
+- [ ] **Step 2: 编写历史表 DDL**
+- [ ] **Step 3: 添加必要索引与约束**
+- [ ] **Step 4: 编写角色迁移 SQL (`admin -> super_admin`)**
+- [ ] **Step 5: 编写测试运营管理员与授权示例 SQL**
+- [ ] **Step 6: 在测试库执行并验证建表/插数成功**
+
+Run: `mysql -u<user> -p<pass> sanqiang < backend/scripts/migration_20260429_user_scope_menu_code.sql`  
+Expected: DDL/DML 全部执行成功,无主键或唯一冲突。
+
+---
+
+### Task 2: 后端模型与 CRUD 支撑
+
+**Files:**
+- Modify: `backend/app/models/models.py`
+- Create: `backend/app/crud/user_region_scope.py`
+- Create: `backend/app/crud/user_menu_scope.py`
+- Modify: `backend/app/crud/__init__.py`
+- Test: `backend/app/crud/user.py`
+
+- [ ] **Step 1: 在模型中新增 `SysUserRegionScope`、`SysUserRegionScopeHis`**
+- [ ] **Step 2: 在模型中新增 `SysUserMenuScope`、`SysUserMenuScopeHis`**
+- [ ] **Step 3: 新建 CRUD,支持按 user_id 查询/全量覆盖更新**
+- [ ] **Step 4: CRUD 更新逻辑增加历史表写入**
+- [ ] **Step 5: 补齐角色枚举 `super_admin` 相关常量**
+
+Run: `python -m py_compile backend/app/models/models.py backend/app/crud/user_region_scope.py backend/app/crud/user_menu_scope.py`  
+Expected: 无语法错误。
+
+---
+
+### Task 3: 认证与授权基础信息扩展
+
+**Files:**
+- Modify: `backend/app/core/auth.py`
+- Modify: `backend/app/api/public.py`
+- Create: `backend/app/core/menu_permission.py`
+- Test: `backend/app/api/public.py`
+
+- [ ] **Step 1: 扩展 current-user 返回 `isSuperAdmin` 与 `regionIds`**
+- [ ] **Step 2: 新增 `GET /web/auth/menu-codes` 接口**
+- [ ] **Step 3: 新增读取用户菜单 code 列表逻辑**
+- [ ] **Step 4: 超级管理员返回全量标识**
+
+Run: `pytest -q backend/tests`  
+Expected: 认证相关测试通过(若缺失测试,至少手工验证接口返回结构)。
+
+---
+
+### Task 4: 菜单 code 查询与菜单管理接口
+
+**Files:**
+- Modify: `backend/app/api/admin.py`
+- Modify: `backend/app/api/public.py`
+- Modify: `backend/app/api/menu.py`
+- Test: `backend/app/main.py`
+
+- [ ] **Step 1: 完成 `GET /web/auth/menu-codes` 接口**
+- [ ] **Step 2: 完成菜单授权 CRUD 接口(按 code)**
+- [ ] **Step 3: 与 `super_admin` 全量菜单逻辑打通**
+- [ ] **Step 4: 校验菜单 code 返回结构与前端静态路由一致**
+- [ ] **Step 5: 保持接口侧不引入 menu-code 鉴权**
+
+Run: `uvicorn backend.app.main:app --reload`  
+Expected: 服务启动成功,菜单 code 接口返回正确。
+
+---
+
+### Task 5: 区域多选与区域范围守卫
+
+**Files:**
+- Modify: `backend/app/api/admin.py`
+- Modify: `backend/app/crud/user.py`
+- Modify: `backend/app/schemas/user.py`
+- Test: `backend/app/api/admin.py`
+
+- [ ] **Step 1: 用户新增/编辑接口支持 `regionIds` 入参**
+- [ ] **Step 2: 新增 `admin` 可见区域集合查询逻辑**
+- [ ] **Step 3: 区域相关查询统一应用 `allowed_region_ids`**
+- [ ] **Step 4: 请求传入越权 `regionId` 时返回 403**
+- [ ] **Step 5: 保持 `super_admin` 不受区域限制**
+
+Run: `pytest -q backend/tests`  
+Expected: 用户与区域权限相关用例通过;越权返回 403。
+
+---
+
+### Task 6: 用户管理页面改造(前端)
+
+**Files:**
+- Modify: `web-admin/src/views/system/user/index.vue`
+- Modify: `web-admin/src/api/admin.ts`
+- Modify: `web-admin/src/api/system-manage.ts`
+- Create: `web-admin/src/views/system/user/modules/user-menu-scope-dialog.vue`
+- Test: `web-admin/src/views/system/user/index.vue`
+
+- [ ] **Step 1: 新增双层 Tab(运营管理人员/区域用户 + 区域 Tab)**
+- [ ] **Step 2: 新增/编辑 `admin` 弹窗区域改为多选**
+- [ ] **Step 3: 新增“菜单权限管理”操作与弹窗**
+- [ ] **Step 4: 对接菜单 code 授权接口**
+- [ ] **Step 5: 对接 `regionIds` 保存接口**
+
+Run: `pnpm --dir web-admin dev`  
+Expected: 页面可正常操作,新增/编辑/授权流程完整。
+
+---
+
+### Task 7: 左侧菜单 code 过滤(前端静态菜单模式)
+
+**Files:**
+- Modify: `web-admin/src/store/modules/menu.ts`
+- Modify: `web-admin/src/router/core/MenuProcessor.ts`
+- Modify: `web-admin/src/router/guards/beforeEach.ts`
+- Modify: `web-admin/src/api/auth.ts`
+- Test: `web-admin/src/router/modules/sqxp.ts`
+
+- [ ] **Step 1: 登录后调用 `GET /web/auth/menu-codes`**
+- [ ] **Step 2: 在本地静态菜单树按 code 过滤**
+- [ ] **Step 3: 实现“仅补齐顶级父菜单”规则**
+- [ ] **Step 4: 无菜单权限展示空状态页面**
+- [ ] **Step 5: `super_admin` 直接全菜单**
+
+Run: `pnpm --dir web-admin build`  
+Expected: 构建通过,路由注册无异常。
+
+---
+
+### Task 8: 迁移执行与回归验证
+
+**Files:**
+- Modify: `docs/superpowers/specs/2026-04-29-web-admin-super-admin-menu-region-design.md`
+- Modify: `docs/后端接口全覆盖测试与数据库校验实施跟踪.md`
+- Modify: `docs/P0-实际验证记录.md`
+- Test: `backend/scripts/migration_20260429_user_scope_menu_code.sql`
+
+- [ ] **Step 1: 按割接顺序执行上线前检查**
+- [ ] **Step 2: 执行迁移与测试账号准备**
+- [ ] **Step 3: 执行四类冒烟(超管/运营/菜单/区域)**
+- [ ] **Step 4: 记录接口与数据库前后快照**
+- [ ] **Step 5: 更新文档留痕**
+
+Run: `git status`  
+Expected: 变更包含代码、SQL、文档,且验证记录完整。
+
+---
+
+## Definition of Done
+
+- 超级管理员:全菜单、全区域、全部数据可见。
+- 运营管理人员:仅授权菜单 code 可见,仅授权区域数据可见。
+- 菜单权限仅作用于前端可见性,不做接口级 menu-code 鉴权。
+- 用户管理页支持运营多区域与菜单授权。
+- 迁移 SQL、测试数据 SQL、验证记录文档齐全。

+ 108 - 0
docs/superpowers/specs/2026-04-29-web-admin-dashboard-console-region-tab-and-scope-design.md

@@ -0,0 +1,108 @@
+## Web Admin 工作台 Console 顶部区域 Tab(权限范围汇总)设计
+
+### 背景
+`http://localhost:3006/#/dashboard/console` 的“今日各区域”“今日下单客户”“概览卡片/最新订单/趋势图”等统计数据目前是全局拉取。
+需要新增顶部“区域 Tab”,并按角色展示不同范围的数据:
+
+- `super_admin`:可以看到所有区域 Tab,并且“全部区域”展示全量数据
+- `admin/运营人员`:只能看到其权限范围内的区域 Tab,并且“全部区域”展示其权限范围内区域的数据之和
+
+### 目标(用户可见行为)
+1. Console 页面最顶部新增区域 Tab 栏
+2. Tab 默认选中“全部区域”
+3. 区域 Tab 文案形如:`全部区域`、`区域A`、`区域B`…
+4. 页面展示的数据随 Tab 切换:
+   - 选中单个区域:仅展示该区域数据
+   - 选中“全部区域”:取该角色可见的区域集合进行汇总
+     - 数值按规则求和
+     - “趋势折线”按日期聚合求和
+     - “今日各区域柱状图”按 region 聚合求和
+     - “今日下单客户柱状图”按 client 聚合求和后排序取前 N(N 取当前页面的固定 limit,不引入额外条数规则)
+     - “最新订单”合并后按 `createAt` 倒序排序显示(不引入新增条数规则)
+
+### 非目标
+- 不重构后端接口
+- 不更改“订单列表/配送列表”等其它页面的筛选行为(仅确保 Console 自身数据范围正确)
+
+### 数据与接口
+当前 Console 页面使用的统计接口(均支持 `regionId`):
+- `fetchWorkbenchDashboard({ regionId? })`:概览卡片、recentOrders、todoSnapshots、warningCount 等
+- `fetchOrderTrend({ regionId?, days })`:近 30 日趋势折线(按日期包含 `orderCount` 与 `orderAmount`)
+- `fetchTodayOrdersByRegion({ regionId? })`:今日各区域统计(列表项包含 `regionId/regionName/orderCount/orderAmount`)
+- `fetchTodayOrdersByClient({ regionId?, limit })`:今日下单客户统计(列表项包含 `clientId/clientName/orderAmount/productQuantity`)
+
+新增取数所需的区域列表:
+- `fetchActiveRegions()`:加载启用区域,用于构建 Tab 列表
+
+权限区域来源:
+- `useUserStore().getUserInfo.role`
+- `useUserStore().getUserInfo.regionIds`(多区域)
+- 兼容 `getUserInfo.regionId`(单区域)
+
+### 交互与状态机
+1. UI 状态:
+   - `activeRegionTabKey`:
+     - `all`:代表“全部区域”
+     - `regionId`:代表单个区域
+2. 可见区域集合:
+   - `super_admin`:`visibleRegionIds = allActiveRegionIds`
+   - 运营 `admin`:`visibleRegionIds = userRegionIds`
+3. Tab 默认:
+   - 默认选中 `all`
+
+### 汇总策略(admin 多区域 && all Tab)
+当角色为运营且选中“全部区域”,将对 `visibleRegionIds` 中每个 `regionId` 并行/串行拉取对应接口结果,并汇总:
+
+#### 1) 概览与最新订单(fetchWorkbenchDashboard)
+- 数值字段求和:
+  - `todayOrderCount`、`todayOrderAmount`、`pendingDeliveryCount`、`customerCount`、`supplierCount`、`deliveryWarningCount` 等按字段求和
+- `todoSnapshots`:
+  - 以 `type` 为 key 聚合 `count` 求和
+  - `items` 做去重合并(保留顺序不做强制,展示以去重后数组为准)
+- `recentOrders`:
+  - 合并各区域的 `recentOrders` 数组
+  - 按 `createAt` 倒序排序
+  - 条数:取“单区域接口返回的 recentOrders 数量”为基准(即不额外定义一条新规则;若单区域返回为空则使用当前页面原有行为兜底)
+
+#### 2) 趋势折线(fetchOrderTrend)
+- 对每个区域拉取同样 `days`(页面固定 30)
+- 按 `date` 聚合:
+  - 同一天的 `orderCount` 求和
+  - 同一天的 `orderAmount` 求和
+- 最终输出顺序与接口 date 顺序保持一致
+
+#### 3) 今日各区域柱状图(fetchTodayOrdersByRegion)
+- 合并所有区域返回列表
+- 按 `regionId` 聚合求和:
+  - `orderCount` 求和
+  - `orderAmount` 求和
+- 图表展示顺序:按 `visibleRegionIds` 的顺序渲染(更符合用户直觉)
+
+#### 4) 今日下单客户柱状图(fetchTodayOrdersByClient)
+- 对每个区域拉取 `limit=30`(与现有页面一致)
+- 按 `clientId` 聚合:
+  - `orderAmount` 求和
+  - `productQuantity` 求和
+- 汇总后按 `orderAmount` 倒序排序取前 `limit`(复用现有 limit,不引入新条数规则)
+
+### UI 约束与样式
+- Tab 样式沿用其它页面的 `region-tabs region-tab-strip / tab-button-group / region-tab-btn`
+- Tab 默认选中“全部区域”
+
+### 错误处理
+- 任一区域接口失败:
+  - 保底:该区域结果视为“空集合”
+  - 页面仍展示其它区域汇总结果
+- 若全部区域失败:
+  - 回退到空数据并关闭 loading
+
+### 测试建议(手工验证)
+1. `super_admin`:
+   - Tab 中能看到所有区域 + 全部区域
+   - 切到任一单区域:图表/概览/最新订单都只显示该区域
+   - 切到“全部区域”:恢复全量
+2. `admin`(权限区域 A,B):
+   - Tab 仅展示 A,B(加上全部区域)
+   - 切到“全部区域”:概览数值等于 A + B 的汇总
+   - “趋势折线/今日柱状图/客户柱状图/最新订单”满足上述聚合规则与排序规则
+

+ 357 - 0
docs/superpowers/specs/2026-04-29-web-admin-super-admin-menu-region-design.md

@@ -0,0 +1,357 @@
+# Web-Admin 超级管理员与运营数据/菜单权限改造设计
+
+> 日期:2026-04-29  
+> 需求来源:运营端权限体系升级(用户确认版)
+
+## 1. 已确认需求口径
+
+- 角色体系新增 `super_admin`(超级管理员),可查看所有菜单、所有区域数据。
+- 现有 `admin` 账号统一迁移为 `super_admin`。
+- 运营管理人员(沿用 `admin`)改为“多区域权限”模型。
+- 菜单授权粒度为“页面菜单级”(仅控制菜单/页面可见,不做按钮级权限)。
+- 菜单实现模式补充:`web-admin` 保持“前端静态菜单模式”,后端仅返回授权菜单 `code` 列表。
+- 菜单授权关系补充:运营管理人员与菜单通过 `menu_code` 建立关系并存库。
+- 超级管理员菜单规则:`super_admin` 始终全菜单可见,不受 `code` 授权限制。
+- 父级补齐规则:仅自动补齐顶级菜单;二级及以下菜单必须显式授权。
+- 在 `用户管理` 中:
+  - 运营管理人员新增/编辑时,区域支持多选。
+  - 增加“菜单权限管理”操作,支持从已有菜单中勾选授权。
+  - 页面改造为“运营管理人员 Tab + 区域 Tab”双维度展示。
+- 具备区域概念的菜单需按运营管理人员区域权限做数据隔离:  
+  `询价报价管理`、`订单列表`、`商家结算管理`、`客户结算管理`、`配送列表`、`时效预警`、`用户管理`。
+- `订单列表`、`配送列表`、`时效预警` 参考 `商家结算管理` 顶部区域 Tab 模式,仅展示有权限区域 Tab。
+- 需要向数据库插入测试运营管理员数据。
+
+---
+
+## 2. 现状评估(基于当前代码)
+
+- 当前后台权限校验以 `role == "admin"` 为主,尚无 `super_admin` 角色常量与判断分支。
+- 当前菜单接口 `GET /web/menu/list` 按 `sys_menu` 全量返回,尚未提供“当前用户授权菜单 code 列表”能力。
+- 当前 `sys_user` 只有单值 `region_id`,不支持多区域。
+- 当前 `admin` 模块已存在“管理员按区域过滤”基础逻辑,但依赖“单区域绑定”语义。
+- 当前 `用户管理` 页面弹窗中“所属区域”为单选。
+
+结论:需要同时改造 **角色模型 + 菜单授权模型 + 区域权限模型 + 前后端展示交互**。
+
+---
+
+## 3. 方案对比(2-3 种)
+
+### 方案 A:最小侵入扩展(推荐)
+
+- 新增关联表:
+  - `sys_user_region_scope`(用户-区域多对多)
+  - `sys_user_menu_scope`(用户-菜单 code 多对多)
+- `sys_user.role` 保留字符串角色,新增 `super_admin` 枚举值。
+- 保持前端静态菜单模式,仅新增“按用户返回菜单 code 列表”接口。
+- 复用现有业务接口,在查询入口统一接入“可见区域集合”过滤。
+
+优点:
+- 对现有代码与表结构冲击小,实施风险最低。
+- 便于灰度:先迁移角色,再逐模块切换数据过滤。
+- 后续可扩展按钮级权限(新增 `menu_type=button` 过滤)但本期不实现。
+
+缺点:
+- 权限语义分散在用户维度,不是标准 RBAC(角色-资源)模型。
+
+### 方案 B:标准 RBAC 重构(本期不推荐)
+
+- 新建角色表、角色菜单表、用户角色表、用户区域范围表。
+- 将当前用户 `role` 字段转为兼容冗余字段或废弃。
+
+优点:
+- 体系标准、长期可扩展性最佳。
+
+缺点:
+- 变更面过大,涉及登录态、路由、所有鉴权分支,交付周期长,回归成本高。
+
+### 方案 C:前端本地过滤为主(不推荐)
+
+- 后端继续返回全菜单/全数据,前端按登录信息过滤显示。
+
+优点:
+- 开发快。
+
+缺点:
+- 安全性不足(接口仍可访问未授权数据),不满足权限改造目标。
+
+### 推荐结论
+
+采用 **方案 A**,以“后端强约束 + 前端按授权展示”为原则落地。
+
+---
+
+## 4. 目标架构设计
+
+## 4.1 角色模型
+
+- `super_admin`:全菜单、全区域、全数据。
+- `admin`(运营管理人员):仅可见授权菜单,且仅可见授权区域数据。
+- 其他业务角色(`supplier/client/deliver`)保持原语义。
+
+## 4.2 区域权限模型
+
+- 运营管理人员通过 `sys_user_region_scope` 绑定多个区域。
+- 查询时优先规则:
+  - `super_admin`:不过滤区域。
+  - `admin`:仅允许 `region_id in 可见区域集合`。
+
+## 4.3 菜单权限模型(菜单级 + code 驱动)
+
+- 运营管理人员通过 `sys_user_menu_scope` 绑定可见菜单 `menu_code`。
+- 前端菜单源保持 `web-admin` 本地默认菜单树(静态)。
+- 登录后额外拉取当前用户 `allowedMenuCodes`:
+  - `super_admin`:返回标识 `all=true`(或空集 + `isSuperAdmin=true`),前端不过滤。
+  - `admin`:返回授权 code 集合。
+- 前端过滤规则:
+  - 命中授权 code 的菜单显示;
+  - 顶级父菜单可自动补齐;
+  - 二级及以下不自动补齐,必须显式授权 code。
+- 后端安全规则:
+  - 本期菜单权限仅用于前端可见性控制,不做接口级 `menu_code` 鉴权;
+  - 接口侧仅保留角色与区域权限强校验。
+
+## 4.4 数据权限应用范围
+
+- 必须接入区域过滤的模块:
+  - 询价报价管理
+  - 订单列表
+  - 商家结算管理
+  - 客户结算管理
+  - 配送列表
+  - 时效预警
+  - 用户管理
+
+---
+
+## 5. 数据库设计(满足项目规范)
+
+## 5.1 变更点
+
+### 表 1:`sys_user`(增量)
+
+- `role` 增加可选值:`super_admin`。
+
+### 表 2:`sys_user_region_scope`(新增)
+
+- 用于运营管理人员多区域授权。
+- 字段建议:
+  - `id` VARCHAR(32) PK
+  - `user_id` VARCHAR(32) 非空
+  - `region_id` VARCHAR(32) 非空
+  - `is_del` TINYINT 默认 0
+  - `create_at` DATETIME
+  - `create_by` VARCHAR(32)
+  - `create_name` VARCHAR(100)
+  - `update_at` DATETIME
+  - `update_by` VARCHAR(32)
+- 唯一索引建议:`(user_id, region_id, is_del)`。
+
+### 表 3:`sys_user_region_scope_his`(新增历史表)
+
+- 与主表同结构,去掉 `is_del`,`id` 不做唯一限制,不建索引。
+
+### 表 4:`sys_user_menu_scope`(新增)
+
+- 用于运营管理人员菜单授权。
+- 字段建议:
+  - `id` VARCHAR(32) PK
+  - `user_id` VARCHAR(32) 非空
+  - `menu_code` VARCHAR(100) 非空
+  - `is_del` TINYINT 默认 0
+  - `create_at` DATETIME
+  - `create_by` VARCHAR(32)
+  - `create_name` VARCHAR(100)
+  - `update_at` DATETIME
+  - `update_by` VARCHAR(32)
+- 唯一索引建议:`(user_id, menu_code, is_del)`。
+
+### 表 5:`sys_user_menu_scope_his`(新增历史表)
+
+- 与主表同结构,去掉 `is_del`,`id` 不做唯一限制,不建索引。
+
+---
+
+## 6. 接口改造设计
+
+## 6.1 认证与用户信息
+
+- 登录返回 `userInfo.role` 支持 `super_admin`。
+- `current-user` 返回新增:
+  - `regionIds: string[]`(运营管理人员可见区域)
+  - `isSuperAdmin: boolean`
+
+## 6.2 菜单接口(code 模式)
+
+- 保留 `GET /web/menu/list` 仅用于管理端菜单维护场景(非登录态菜单渲染依赖)。
+- 新增(或替代)接口:
+  - `GET /web/auth/menu-codes`:返回当前登录用户授权菜单 code 集合与 `isSuperAdmin`。
+  - `GET /web/admin/menus/assignable-codes`:可授权菜单 code 列表(含层级信息,来源于 `sys_menu.code`)。
+  - `GET /web/admin/users/{id}/menu-scopes`:查询用户菜单授权 code 列表。
+  - `PUT /web/admin/users/{id}/menu-scopes`:更新用户菜单授权(按 code 全量覆盖)。
+
+## 6.2.1 菜单权限边界(实现约束)
+
+- 本期菜单权限边界:只控制“前端菜单可见性”。
+- 不实现接口级 `menu_code` 鉴权,避免接口粒度过细带来的测试复杂度。
+- 接口安全仍由“角色权限 + 区域权限”承担:
+  - 非运营角色不可访问运营接口;
+  - 运营仅可访问授权区域数据;
+  - 区域越权统一返回 403。
+
+## 6.3 用户管理接口
+
+- `GET /web/admin/users`:
+  - 运营管理人员仅能看到其区域范围内数据(且允许查看超管+运营在“运营管理人员 Tab”)。
+- `POST /web/admin/users` / `PUT /web/admin/users/{id}`:
+  - 对 `admin` 角色支持 `regionIds` 多选写入。
+  - `super_admin` 不要求区域绑定。
+- 角色列表 `GET /web/admin/roles` 增加 `super_admin`。
+
+## 6.4 区域过滤相关业务接口
+
+- 在目标模块列表/详情查询入口统一注入 `allowed_region_ids`。
+- 当请求传入 `regionId` 时必须校验是否属于授权范围,否则返回 403。
+
+---
+
+## 7. 前端(web-admin)交互设计
+
+## 7.1 菜单显示
+
+- 登录后先读取当前用户 `allowedMenuCodes`,再过滤前端默认静态菜单树。
+- `super_admin` 不做菜单过滤,直接展示前端默认全菜单。
+- `admin` 菜单按 code 过滤,且仅自动补齐顶级父菜单。
+- 无任何授权菜单的运营管理人员,展示“暂无菜单权限”空状态页。
+
+## 7.1.1 code 单一真源与一致性(新增)
+
+- 单一真源约定:
+  - 前端菜单渲染真源:`web-admin` 本地静态路由中的 `name/code`。
+  - 后端授权真源:`sys_menu.code`,并要求与前端静态路由 code 一致。
+- 一致性校验机制:
+  - 启动校验(推荐):后端启动时检查 `sys_menu.code` 与约定 code 字典是否存在差异,差异写日志告警。
+  - CI 校验(推荐):新增脚本比对“前端 code 清单 vs 数据库基线 SQL code 清单”,差异时阻断发布。
+- 运维规则:
+  - 新增/修改菜单必须同步更新前端静态菜单 code 与 `sys_menu.code`,否则视为变更不完整。
+
+## 7.2 用户管理页改造
+
+- 顶部改为双层 Tab:
+  - 第一层:`运营管理人员`、`区域用户`
+  - 第二层(区域维度):按当前登录用户可见区域动态显示
+- 运营管理人员 Tab:
+  - 展示 `super_admin` + `admin`
+  - 支持新增/编辑这两类角色
+  - `admin` 编辑弹窗中“所属区域”改为多选
+  - 行操作新增“菜单权限管理”弹窗(树形勾选)
+- 区域用户 Tab:
+  - 展示 `client/supplier/deliver`
+  - 支持新增/编辑这三类角色
+
+## 7.3 订单/配送/时效预警页面
+
+- 参考商家结算管理顶部区域 Tab 风格:
+  - 仅渲染登录用户有权限的区域 Tab。
+  - 切换 Tab 即带入对应 `regionId` 查询。
+- `super_admin` 默认可见全部区域 Tab。
+
+---
+
+## 8. 迁移策略
+
+## 8.1 角色迁移(已确认)
+
+- 一次性将现有 `sys_user.role='admin'` 批量更新为 `super_admin`。
+- 后续由超级管理员创建新的运营管理人员(`admin`)。
+
+## 8.2 回滚策略
+
+- 保留迁移 SQL 与反向 SQL:
+  - 正向:`admin -> super_admin`
+  - 反向:`super_admin -> admin`(仅紧急回滚使用)
+
+## 8.3 割接执行顺序(新增,强约束)
+
+1. 上线数据库增量(`sys_user_region_scope*`、`sys_user_menu_scope*`)与兼容代码(不切主流程)。
+2. 上线菜单 code 接口(仅用于前端菜单过滤),不启用接口级菜单守卫。
+3. 由当前管理员创建测试运营账号并完成区域+菜单授权,完成联调回归。
+4. 执行 `admin -> super_admin` 迁移 SQL(维护窗口内执行)。
+5. 迁移后立即执行冒烟:
+   - `super_admin` 全菜单可见;
+   - 运营账号仅见授权菜单;
+   - 区域外数据不可见。
+6. 通过后进入常规发布;若失败按回滚策略恢复。
+
+---
+
+## 9. 测试数据 SQL(含你要求的测试运营管理员)
+
+```sql
+-- 1) 角色迁移:admin -> super_admin
+UPDATE sys_user
+SET role = 'super_admin', update_at = NOW()
+WHERE is_del = 0 AND role = 'admin';
+
+-- 2) 插入测试运营管理员(admin)
+-- 注意:password_hash 需替换为项目实际哈希算法生成值(示例占位)
+INSERT INTO sys_user (
+  id, username, password_hash, real_name, phone, role, region_id, is_active, is_del,
+  create_at, create_by, create_name, update_at, update_by
+) VALUES
+('op_admin_001_32char_id_xxxxxx01', 'op_admin_a', 'REPLACE_WITH_HASH', '运营A', '13800000001', 'admin', NULL, 1, 0, NOW(), 'seed', 'seed', NOW(), 'seed'),
+('op_admin_002_32char_id_xxxxxx02', 'op_admin_b', 'REPLACE_WITH_HASH', '运营B', '13800000002', 'admin', NULL, 1, 0, NOW(), 'seed', 'seed', NOW(), 'seed');
+
+-- 3) 绑定测试运营管理员多区域(示例)
+INSERT INTO sys_user_region_scope (
+  id, user_id, region_id, is_del, create_at, create_by, create_name, update_at, update_by
+) VALUES
+('urs_001_32char_id_xxxxxxxxxx01', 'op_admin_001_32char_id_xxxxxx01', 'region_id_east', 0, NOW(), 'seed', 'seed', NOW(), 'seed'),
+('urs_002_32char_id_xxxxxxxxxx02', 'op_admin_001_32char_id_xxxxxx01', 'region_id_west', 0, NOW(), 'seed', 'seed', NOW(), 'seed'),
+('urs_003_32char_id_xxxxxxxxxx03', 'op_admin_002_32char_id_xxxxxx02', 'region_id_south', 0, NOW(), 'seed', 'seed', NOW(), 'seed');
+
+-- 4) 绑定测试运营管理员菜单权限(示例,menu_code 需替换为 sys_menu.code)
+INSERT INTO sys_user_menu_scope (
+  id, user_id, menu_code, is_del, create_at, create_by, create_name, update_at, update_by
+) VALUES
+('ums_001_32char_id_xxxxxxxxxx01', 'op_admin_001_32char_id_xxxxxx01', 'Order', 0, NOW(), 'seed', 'seed', NOW(), 'seed'),
+('ums_002_32char_id_xxxxxxxxxx02', 'op_admin_001_32char_id_xxxxxx01', 'OrderList', 0, NOW(), 'seed', 'seed', NOW(), 'seed'),
+('ums_003_32char_id_xxxxxxxxxx03', 'op_admin_002_32char_id_xxxxxx02', 'Delivery', 0, NOW(), 'seed', 'seed', NOW(), 'seed');
+```
+
+---
+
+## 10. 验收标准(UAT)
+
+- 超级管理员登录后:
+  - 可见全部菜单、全部区域 Tab、全部数据。
+- 运营管理人员登录后:
+  - 仅可见授权菜单。
+  - 仅可见授权区域 Tab。
+  - 仅可查询授权区域的数据(接口层强校验)。
+- 用户管理:
+  - 可为运营管理人员配置多区域。
+  - 可为运营管理人员配置菜单权限。
+  - 双层 Tab 交互与展示符合定义。
+- 未授权访问:
+  - 前端菜单不可见;
+  - 后端仅对区域越权返回 403。
+
+---
+
+## 11. 风险与控制
+
+- 风险 1:code 过滤后菜单层级不完整导致导航异常。  
+  控制:仅自动补齐顶级父菜单,授权弹窗中明确提示“二级及以下需显式授权”。
+- 风险 2:区域过滤遗漏导致越权。  
+  控制:抽取统一 `allowed_region_ids` 过滤函数,在模块接口层强制调用。
+- 风险 3:历史 `admin` 全量迁移后短期无人可用。  
+  控制:迁移脚本与测试运营管理员插入脚本同批执行。
+
+---
+
+## 12. 本设计结论
+
+本期按“**超级管理员全权限 + 前端静态菜单(code 过滤) + 运营管理人员多区域数据权限**”落地;菜单权限仅作用于前端可见性,接口侧保持角色与区域权限校验。
+

BIN
docs/superpowers/uat-home.png


+ 146 - 0
docs/superpowers/uat-role-region-menu-2026-04-29.json

@@ -0,0 +1,146 @@
+{
+  "super_admin": {
+    "username": "admin",
+    "login_status": 200,
+    "login_code": 200,
+    "login_message": "success",
+    "checks": {
+      "current_user": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "regionIds": [],
+        "role": "super_admin",
+        "isSuperAdmin": true
+      },
+      "menu_codes": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "menuCodes": [],
+        "isSuperAdmin": true
+      },
+      "users_default": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 10,
+        "total": 23
+      },
+      "users_rolein_region": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 10,
+        "total": 21
+      },
+      "users_rolein_ops": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 2,
+        "total": 2
+      },
+      "orders": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 10,
+        "total": 34
+      },
+      "delivery": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 10,
+        "total": 12
+      },
+      "warning": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 1,
+        "total": 1
+      }
+    }
+  },
+  "multi_admin": {
+    "username": "op_admin_multi",
+    "login_status": 200,
+    "login_code": 200,
+    "login_message": "success",
+    "checks": {
+      "current_user": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "regionIds": [
+          "1A632F3CD4364598AD80AD65DEBAF0CC",
+          "3349ED3D7C2B44A6B944AA2916542785"
+        ],
+        "role": "admin",
+        "isSuperAdmin": false
+      },
+      "menu_codes": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "menuCodes": [
+          "dashboard",
+          "delivery_list",
+          "delivery_warning",
+          "order_list",
+          "user"
+        ],
+        "isSuperAdmin": false
+      },
+      "users_default": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 0,
+        "total": 0
+      },
+      "users_rolein_region": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 0,
+        "total": 0
+      },
+      "users_rolein_ops": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 0,
+        "total": 0
+      },
+      "orders": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 0,
+        "total": 0
+      },
+      "delivery": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 10,
+        "total": 12
+      },
+      "warning": {
+        "http": 200,
+        "code": 200,
+        "message": "success",
+        "list_len": 1,
+        "total": 1
+      }
+    },
+    "unauthorized_region": {
+      "http": 200,
+      "code": 403,
+      "message": "无权访问该区域数据"
+    }
+  }
+}

BIN
docs/superpowers/uat-screens/debug-after-login.png


BIN
docs/superpowers/uat-screens/debug-login-page.png


BIN
docs/superpowers/uat-screens/multi-admin-error.png


BIN
docs/superpowers/uat-screens/super-admin-error.png


+ 24 - 0
docs/superpowers/uat-ui-role-check-2026-04-29.json

@@ -0,0 +1,24 @@
+{
+  "super_admin": {
+    "label": "super-admin",
+    "username": "admin",
+    "checks": {},
+    "screenshots": {
+      "error": "/Users/wuzilong/work/00code/sqxp_副本2/docs/superpowers/uat-screens/super-admin-error.png"
+    },
+    "errors": [
+      "login redirected to 500: http://127.0.0.1:5173/login#/500"
+    ]
+  },
+  "multi_admin": {
+    "label": "multi-admin",
+    "username": "op_admin_multi",
+    "checks": {},
+    "screenshots": {
+      "error": "/Users/wuzilong/work/00code/sqxp_副本2/docs/superpowers/uat-screens/multi-admin-error.png"
+    },
+    "errors": [
+      "login redirected to 500: http://127.0.0.1:5173/login#/500"
+    ]
+  }
+}

+ 159 - 193
graphify-out/GRAPH_REPORT.md

@@ -1,12 +1,12 @@
 # Graph Report - sqxp_副本2  (2026-04-29)
 
 ## Corpus Check
-- 699 files · ~594,863 words
+- 700 files · ~634,064 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 4127 nodes · 9409 edges · 66 communities detected
-- Extraction: 53% EXTRACTED · 47% INFERRED · 0% AMBIGUOUS · INFERRED: 4414 edges (avg confidence: 0.7)
+- 4643 nodes · 21509 edges · 58 communities detected
+- Extraction: 24% EXTRACTED · 76% INFERRED · 0% AMBIGUOUS · INFERRED: 16425 edges (avg confidence: 0.56)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -32,399 +32,365 @@
 - [[_COMMUNITY_Community 19|Community 19]]
 - [[_COMMUNITY_Community 20|Community 20]]
 - [[_COMMUNITY_Community 21|Community 21]]
-- [[_COMMUNITY_Community 22|Community 22]]
-- [[_COMMUNITY_Community 23|Community 23]]
 - [[_COMMUNITY_Community 24|Community 24]]
 - [[_COMMUNITY_Community 25|Community 25]]
 - [[_COMMUNITY_Community 26|Community 26]]
 - [[_COMMUNITY_Community 27|Community 27]]
-- [[_COMMUNITY_Community 28|Community 28]]
 - [[_COMMUNITY_Community 29|Community 29]]
-- [[_COMMUNITY_Community 30|Community 30]]
+- [[_COMMUNITY_Community 31|Community 31]]
+- [[_COMMUNITY_Community 32|Community 32]]
 - [[_COMMUNITY_Community 33|Community 33]]
 - [[_COMMUNITY_Community 34|Community 34]]
-- [[_COMMUNITY_Community 35|Community 35]]
 - [[_COMMUNITY_Community 36|Community 36]]
 - [[_COMMUNITY_Community 37|Community 37]]
 - [[_COMMUNITY_Community 38|Community 38]]
-- [[_COMMUNITY_Community 40|Community 40]]
-- [[_COMMUNITY_Community 42|Community 42]]
-- [[_COMMUNITY_Community 43|Community 43]]
+- [[_COMMUNITY_Community 39|Community 39]]
 - [[_COMMUNITY_Community 44|Community 44]]
-- [[_COMMUNITY_Community 45|Community 45]]
 - [[_COMMUNITY_Community 46|Community 46]]
+- [[_COMMUNITY_Community 47|Community 47]]
 - [[_COMMUNITY_Community 48|Community 48]]
 - [[_COMMUNITY_Community 49|Community 49]]
+- [[_COMMUNITY_Community 50|Community 50]]
+- [[_COMMUNITY_Community 51|Community 51]]
+- [[_COMMUNITY_Community 53|Community 53]]
 - [[_COMMUNITY_Community 54|Community 54]]
-- [[_COMMUNITY_Community 56|Community 56]]
-- [[_COMMUNITY_Community 57|Community 57]]
-- [[_COMMUNITY_Community 58|Community 58]]
+- [[_COMMUNITY_Community 55|Community 55]]
 - [[_COMMUNITY_Community 59|Community 59]]
-- [[_COMMUNITY_Community 61|Community 61]]
+- [[_COMMUNITY_Community 62|Community 62]]
 - [[_COMMUNITY_Community 63|Community 63]]
-- [[_COMMUNITY_Community 64|Community 64]]
-- [[_COMMUNITY_Community 68|Community 68]]
-- [[_COMMUNITY_Community 71|Community 71]]
-- [[_COMMUNITY_Community 72|Community 72]]
-- [[_COMMUNITY_Community 74|Community 74]]
-- [[_COMMUNITY_Community 86|Community 86]]
-- [[_COMMUNITY_Community 96|Community 96]]
-- [[_COMMUNITY_Community 98|Community 98]]
-- [[_COMMUNITY_Community 152|Community 152]]
-- [[_COMMUNITY_Community 153|Community 153]]
-- [[_COMMUNITY_Community 154|Community 154]]
-- [[_COMMUNITY_Community 155|Community 155]]
-- [[_COMMUNITY_Community 156|Community 156]]
-- [[_COMMUNITY_Community 157|Community 157]]
+- [[_COMMUNITY_Community 65|Community 65]]
+- [[_COMMUNITY_Community 77|Community 77]]
+- [[_COMMUNITY_Community 87|Community 87]]
+- [[_COMMUNITY_Community 89|Community 89]]
+- [[_COMMUNITY_Community 143|Community 143]]
+- [[_COMMUNITY_Community 144|Community 144]]
+- [[_COMMUNITY_Community 145|Community 145]]
+- [[_COMMUNITY_Community 146|Community 146]]
+- [[_COMMUNITY_Community 147|Community 147]]
+- [[_COMMUNITY_Community 148|Community 148]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `success_response()` - 339 edges
-2. `query()` - 260 edges
-3. `error_response()` - 236 edges
-4. `CRUDBase` - 82 edges
-5. `BizPriceListItem` - 80 edges
-6. `BizOrder` - 70 edges
-7. `push()` - 68 edges
-8. `BizDeliveryTask` - 63 edges
-9. `BasProduct` - 62 edges
-10. `SysUser` - 61 edges
+1. `BizPriceListItem` - 541 edges
+2. `SysUser` - 534 edges
+3. `BizOrder` - 531 edges
+4. `BizDeliveryTask` - 519 edges
+5. `BasProduct` - 518 edges
+6. `SysRegion` - 511 edges
+7. `BizInquiry` - 509 edges
+8. `BizOrderItem` - 509 edges
+9. `BasProductCategory` - 508 edges
+10. `BasProductUnit` - 508 edges
 
 ## Surprising Connections (you probably didn't know these)
 - `get_active()` --calls--> `query()`  [INFERRED]
   backend/app/models/base.py → sqxp-wx/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue
-- `get()` --calls--> `query()`  [INFERRED]
-  backend/app/crud/api_request_log.py → sqxp-wx/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue
 - `register()` --calls--> `validate()`  [INFERRED]
   web-admin/src/views/auth/register/index.vue → sqxp-wx/pages/client/address/edit.vue
-- `goToWarning()` --calls--> `push()`  [INFERRED]
-  web-admin/src/views/dashboard/console/index.vue → sqxp-wx/common/permission.js
-- `handleOverviewClick()` --calls--> `push()`  [INFERRED]
-  web-admin/src/views/dashboard/console/index.vue → sqxp-wx/common/permission.js
+- `toTime()` --calls--> `getTime()`  [INFERRED]
+  web-admin/src/views/dashboard/console/index.vue → sqxp-wx/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js
+- `number()` --calls--> `inputMaxlength()`  [INFERRED]
+  web-admin/src/views/system/inquiry-quotation/index.vue → sqxp-wx/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue
+- `number()` --calls--> `onPickerChange()`  [INFERRED]
+  web-admin/src/views/system/inquiry-quotation/index.vue → sqxp-wx/pages/client/invoice/invoice.vue
 
 ## Communities
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (446): adjust_order(), _aggregate_client_settlement_history(), _aggregate_supplier_settlement_history(), _apply_region_minutes_to_data(), approve_quotation(), assign_deliverer_to_order(), assign_delivery_alias(), batch_create_settlements() (+438 more)
+Nodes (608): adjust_order(), _aggregate_client_settlement_history(), _aggregate_supplier_settlement_history(), _apply_region_minutes_to_data(), approve_quotation(), assign_deliverer_to_order(), assign_delivery_alias(), batch_create_settlements() (+600 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.01
-Nodes (135): fullRegion(), if(), if(), FireworkSystem, mapKeys(), VantComponent(), HTMLParser(), parseAttrs() (+127 more)
+Cohesion: 0.08
+Nodes (549): 获取管理员的区域过滤条件      对于区域管理员,返回其所属区域ID(用于过滤数据)     对于超级管理员(无区域绑定),返回None(不过滤), 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 与 PUT 同逻辑:前端按 POST 模式调用时复用。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 更新用户菜单授权(按 code 全量覆盖;兼容明文/密文载荷)。, 兼容旧逻辑:返回单区域过滤ID。多区域场景返回排序后首个区域。, 上传用户头像图片,返回可写入 avatarUrl 的地址 (+541 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.01
-Nodes (247): InvoiceCreateRequest, MessageCreateRequest, 获取商品的评价列表(仅显示已审核通过的评价), AdjustResponse, Config, CreateAdjustRequest, BaseModel, Config (+239 more)
+Nodes (141): fullRegion(), if(), if(), FireworkSystem, mapKeys(), VantComponent(), HTMLParser(), parseAttrs() (+133 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.02
-Nodes (90): _clamp_to_meta(), ContactSettingsUpdate, ensure_web_system_settings_rows(), get_auto_cancel_minutes_for_job(), get_auto_confirm_receive_days_for_job(), get_default_region_expected_delivery_minutes(), get_system_settings(), _normalize_service_phone() (+82 more)
+Cohesion: 0.01
+Nodes (153): reload(), calendar(), memo(), record(), addUnit(), isDef(), isNumber(), nextTick() (+145 more)
 
 ### Community 4 - "Community 4"
 Cohesion: 0.01
-Nodes (103): handleSubmit(), loadDetail(), onRegionMultiConfirm(), regionLabel(), syncMultiFromForm(), validate(), disableDevTools(), handleLock() (+95 more)
+Nodes (248): InvoiceCreateRequest, MessageCreateRequest, 获取商品的评价列表(仅显示已审核通过的评价), AdjustResponse, Config, CreateAdjustRequest, 兼容明文/密文请求,返回业务 payload。失败时返回 error_response。, BaseModel (+240 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.02
-Nodes (120): if(), register(), validateAgreement(), todayText(), at(), ct(), dt(), e() (+112 more)
+Cohesion: 0.01
+Nodes (195): # TODO: 发送微信模板消息提醒配送员, 记录处理过程(不结案,结案由配送完成/取消自动触发), 小程序客户端扩展接口:购物车、商品分类、仪表盘、发票、下单、支付, 可申请开票的已完成订单(简化:已完成且尚无发票), 商品对应的一级(根)分类,供购物车行展示与序列化。, 购物车:先一级分类 sort_order,再商品名、商品 id(稳定)。, 当前用户有效购物车行(含区域价目表同步、过滤已删)。, 将购物车 ORM 行序列化为与 GET /cart 一致的 list 项(仅未删除行)。 (+187 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.07
-Nodes (172): 结算相关接口:区域管理员固定为其绑定区域;超级管理员可按 Tab 的 regionId 筛选。, 各启用区域下,当前生效价格清单中该商品的单价(无生效价则 price 为空), 已生成客户结算单:结算金额合计、订单笔数合计(与结算单主表 order_count 汇总一致)。, 已生成供应商结算单:结算金额合计、订单笔数合计。, 取消发布询价单(published -> draft), # NOTE: older price lists may have NULL pricing_coefficient; we keep None to all, 解析区域配送相关分钟配置;键不存在返回 _REGION_MINUTES_UNSET,空值为 None。, 按询价单聚合:每个供应商最新一次报价(包含未报价供应商) (+164 more)
+Cohesion: 0.01
+Nodes (136): fetchActiveRegions(), fetchAssignableMenuCodes(), fetchCategoryList(), fetchCategoryParameters(), fetchCategoryTree(), fetchCreateUnit(), fetchNotDeletedRegions(), fetchProductBannerList() (+128 more)
 
 ### Community 7 - "Community 7"
-Cohesion: 0.02
-Nodes (46): fetchGetUserInfo(), fetchLogin(), fetchGetMenuList(), fetchGetRoleList(), fetchGetUserList(), reload(), nextTick(), useSettingsHandlers() (+38 more)
+Cohesion: 0.01
+Nodes (129): handleSubmit(), loadDetail(), onRegionMultiConfirm(), regionLabel(), syncMultiFromForm(), validate(), close_payment(), create_wechat_pay() (+121 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.03
-Nodes (43): calendar(), memo(), record(), handleSelect(), onMenuShow(), showMenu(), onClear(), onInput() (+35 more)
+Cohesion: 0.02
+Nodes (111): endDate(), selected(), startDate(), Calendar, changeFlag(), countDown(), day(), handler() (+103 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.03
-Nodes (59): get_china_region_tree_admin(), BasChinaRegion, 中国行政区划(GB/T 2260),见 docs/sql/bas_china_region.sql, build_china_region_tree(), get_china_region_tree_cached(), query_china_regions_active(), 中国行政区划(bas_china_region)树形结构,供运营端与小程序共用。, { value: region_code, label: region_name, children?: [...] },与历史 china-region-ca (+51 more)
+Cohesion: 0.02
+Nodes (67): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchGetMenuList(), fetchGetRoleList(), fetchGetUserList(), useSettingsHandlers(), useSettingsPanel() (+59 more)
 
 ### Community 10 - "Community 10"
 Cohesion: 0.03
-Nodes (54): Q(), X(), Y(), remove(), change(), created(), getDataList(), getSelectedValue() (+46 more)
+Nodes (43): askAbortDownload(), checkLocalStoragePackage(), closeUpdate(), compare(), deleteSavedFile(), downLoadComplete(), installPackage(), onLoad() (+35 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.02
-Nodes (68): fetchActiveRegions(), fetchAdjustableOrders(), fetchApiRequestLogList(), fetchClientSettlementHistorySummary(), fetchClientSettlementOrderList(), fetchClientSettlementSheetDetail(), fetchClientSettlementSheetList(), fetchCloseInquiry() (+60 more)
+Cohesion: 0.05
+Nodes (15): fetchGetLatestUpgradeLog(), if(), getDefaultLanguage(), emitUrls(), removeItem(), revokeLocalPreview(), checkStorageCompatibility(), StorageConfig (+7 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.03
-Nodes (47): lifespan(), BaseStorage, convert_heic_to_jpeg(), get_storage(), LocalStorage, MinIOStorage, OSSStorage, process_image_content() (+39 more)
+Cohesion: 0.09
+Nodes (27): base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes(), sha256Hex() (+19 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.02
-Nodes (43): fetchCategoryList(), fetchCategoryParameters(), fetchCategoryTree(), fetchCreateUnit(), fetchProductBannerList(), fetchProductDetail(), fetchProductDetailMediaList(), fetchProductList() (+35 more)
+Cohesion: 0.11
+Nodes (12): activated(), bottom(), center(), close(), deactivated(), destroyed(), left(), onTap() (+4 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.04
-Nodes (32): InvoiceCRUD, derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, ThreeStatus, _to_decimal(), auto_resolve_open_warnings_for_task(), 配送预警:任务完成/取消时自动结案(写历史后更新主表), 将某配送任务下未结案预警全部自动处理(配送完成等场景)。 (+24 more)
+Cohesion: 0.15
+Nodes (19): 备货完成后通知配送员(微信订阅消息,失败不阻断主流程), 供应商开始备货:assigned -> supplier_stocking,并写入订单状态日志(配送进度), 供应商备货完成:supplier_stocking -> supplier_ready(待配送员取货),写入订单日志并可通知配送员, [兼容] 请改用 POST /tasks/{id}/start-stock, [兼容] 请改用 POST /tasks/{id}/finish-stock, 供应商端商品详情:轮播、基础信息、参数、富文本、详情媒体, 价格清单行 price 为面向客户的最终单价;pricing_coefficient 为调价系数(%),供货单价 = price / (1 + 系数/100)。, 获取供应商的配送任务列表(含商品缩略列表;供货合计按价格清单还原供货单价后汇总) (+11 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.04
-Nodes (23): fetchDeleteComment(), fetchDeleteUpgradeLog(), fetchGetAllUpgradeLogs(), fetchGetChinaMapData(), fetchGetComments(), fetchGetCurrentVersion(), fetchGetLatestUpgradeLog(), fetchGetSystemConfig() (+15 more)
-
-### Community 16 - "Community 16"
-Cohesion: 0.05
-Nodes (56): _avatar_url_field(), logout(), mini_bind_wechat(), mini_change_password(), mini_code_openid(), mini_login(), mini_login_rsa_public_key(), mini_logout() (+48 more)
-
-### Community 17 - "Community 17"
-Cohesion: 0.04
-Nodes (29): addUnit(), getSystemInfoSync(), isDef(), isNumber(), setSizeWithUnit(), setSizeWithUnit(), setTextSizeWithUnit(), date() (+21 more)
-
-### Community 18 - "Community 18"
-Cohesion: 0.1
-Nodes (26): close_payment(), create_wechat_pay(), get_pay_config(), _parse_wechat_pay_time(), query_payment_status(), 创建微信支付订单      返回调起微信支付所需的参数, 查询支付状态      用于前端轮询查询支付结果, 微信支付回调通知      注意:此接口不需要认证 (+18 more)
-
-### Community 19 - "Community 19"
-Cohesion: 0.1
-Nodes (27): BaseHTTPMiddleware, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json(), derive_key(), encrypt_json(), gen_nonce_hex() (+19 more)
-
-### Community 20 - "Community 20"
-Cohesion: 0.09
-Nodes (26): base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes(), sha256Hex() (+18 more)
-
-### Community 21 - "Community 21"
-Cohesion: 0.11
-Nodes (12): activated(), bottom(), center(), close(), deactivated(), destroyed(), left(), onTap() (+4 more)
-
-### Community 22 - "Community 22"
 Cohesion: 0.18
 Nodes (21): destroyed(), getDirection(), getDom(), isPC(), mousedown(), mouseleave(), mousemove(), mouseup() (+13 more)
 
-### Community 23 - "Community 23"
+### Community 16 - "Community 16"
 Cohesion: 0.13
 Nodes (13): change(), clearVal(), created(), debounce(), emit(), formatItemName(), getCache(), handler() (+5 more)
 
-### Community 24 - "Community 24"
+### Community 17 - "Community 17"
 Cohesion: 0.12
 Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
-### Community 25 - "Community 25"
-Cohesion: 0.17
-Nodes (12): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+4 more)
-
-### Community 26 - "Community 26"
+### Community 18 - "Community 18"
 Cohesion: 0.17
 Nodes (16): animation(), bindTiming(), close(), getDom(), getEl(), getSelectorQuery(), init(), leftOptions() (+8 more)
 
-### Community 27 - "Community 27"
+### Community 19 - "Community 19"
 Cohesion: 0.18
 Nodes (16): defaultAvatarPathByRole(), normalizeAvatarUrlForMini(), resolveUserAvatarUrl(), appendQueryParam(), fallbackVersionFromManifest(), getStaticAssetsVersion(), hasSignedQuery(), hasVersionParam() (+8 more)
 
-### Community 28 - "Community 28"
+### Community 20 - "Community 20"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 29 - "Community 29"
-Cohesion: 0.27
-Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
-
-### Community 30 - "Community 30"
+### Community 21 - "Community 21"
 Cohesion: 0.28
 Nodes (14): getDirection(), getDisabledType(), getDom(), move(), moveDirection(), openState(), range(), resetTouchStatus() (+6 more)
 
-### Community 33 - "Community 33"
+### Community 24 - "Community 24"
 Cohesion: 0.17
 Nodes (2): clean(), onInput()
 
-### Community 34 - "Community 34"
-Cohesion: 0.33
-Nodes (11): changeFlag(), countDown(), day(), handler(), hour(), minute(), second(), startData() (+3 more)
-
-### Community 35 - "Community 35"
-Cohesion: 0.27
-Nodes (8): 微信消息通知服务  支持微信小程序模板消息推送  使用方式:     from app.core.wechat_message import Wech, 发送模板消息                  Args:             openid: 用户openid             templ, send_delivery_notice(), send_delivery_warning(), send_order_status(), send_payment_success(), send_refund_result(), send_urge_notification()
-
-### Community 36 - "Community 36"
+### Community 25 - "Community 25"
 Cohesion: 0.24
 Nodes (4): getVal(), leftIconWidth(), navbarHeight(), rightIconWidth()
 
-### Community 37 - "Community 37"
+### Community 26 - "Community 26"
 Cohesion: 0.4
 Nodes (9): backup_table(), count_affected(), get_text_columns(), main(), make_backup_table_name(), parse_args(), quote_ident(), 生成备份表名,控制在 MySQL 64 字符限制内。 (+1 more)
 
-### Community 38 - "Community 38"
+### Community 27 - "Community 27"
 Cohesion: 0.24
 Nodes (3): _calcValue(), _getDecimalScale(), _onBlur()
 
-### Community 40 - "Community 40"
+### Community 29 - "Community 29"
 Cohesion: 0.27
 Nodes (4): dataValue(), emit(), onChange(), setOpen()
 
-### Community 42 - "Community 42"
+### Community 31 - "Community 31"
 Cohesion: 0.25
 Nodes (2): borderStyle(), value2px()
 
-### Community 43 - "Community 43"
-Cohesion: 0.29
-Nodes (3): BaseModel, get_active(), SoftDeleteModel
-
-### Community 44 - "Community 44"
+### Community 32 - "Community 32"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
 
-### Community 45 - "Community 45"
+### Community 33 - "Community 33"
 Cohesion: 0.29
 Nodes (2): handleTouchEnd(), setTransition()
 
-### Community 46 - "Community 46"
+### Community 34 - "Community 34"
 Cohesion: 0.36
 Nodes (1): TableHeightCalculator
 
-### Community 48 - "Community 48"
+### Community 36 - "Community 36"
+Cohesion: 0.36
+Nodes (1): RouteValidator
+
+### Community 37 - "Community 37"
 Cohesion: 0.39
 Nodes (5): chooseAll(), chooseAndUploadFile(), chooseImage(), chooseVideo(), uploadFiles()
 
-### Community 49 - "Community 49"
+### Community 38 - "Community 38"
+Cohesion: 0.33
+Nodes (2): logApiCryptoBootInfo(), maskSecret()
+
+### Community 39 - "Community 39"
 Cohesion: 0.38
 Nodes (3): _change(), close(), open()
 
-### Community 54 - "Community 54"
+### Community 44 - "Community 44"
 Cohesion: 0.33
 Nodes (1): BridgeError
 
-### Community 56 - "Community 56"
+### Community 46 - "Community 46"
 Cohesion: 0.4
 Nodes (2): created(), getTable()
 
-### Community 57 - "Community 57"
+### Community 47 - "Community 47"
 Cohesion: 0.47
 Nodes (3): get_file_data(), get_file_ext(), get_file_info()
 
-### Community 58 - "Community 58"
+### Community 48 - "Community 48"
 Cohesion: 0.4
 Nodes (2): created(), getForm()
 
-### Community 59 - "Community 59"
+### Community 49 - "Community 49"
 Cohesion: 0.47
 Nodes (4): onDial(), onTextClick(), dialPhone(), normalizeDialNumber()
 
-### Community 61 - "Community 61"
+### Community 50 - "Community 50"
+Cohesion: 0.4
+Nodes (4): 运营端角色守卫:admin / super_admin 可访问。, 接口菜单权限守卫。super_admin 直接放行。, require_admin_or_super_admin(), require_menu_code()
+
+### Community 51 - "Community 51"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 63 - "Community 63"
+### Community 53 - "Community 53"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 64 - "Community 64"
+### Community 54 - "Community 54"
 Cohesion: 0.67
 Nodes (3): build_menu_tree(), get_menu_list(), 获取菜单列表(从 sys_menu 表读取,构建树形结构)
 
-### Community 68 - "Community 68"
+### Community 55 - "Community 55"
+Cohesion: 0.5
+Nodes (1): collectIds()
+
+### Community 59 - "Community 59"
 Cohesion: 0.83
 Nodes (3): classes(), isTrue(), onClick()
 
-### Community 71 - "Community 71"
+### Community 62 - "Community 62"
 Cohesion: 0.67
 Nodes (2): created(), getParent()
 
-### Community 72 - "Community 72"
+### Community 63 - "Community 63"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 74 - "Community 74"
+### Community 65 - "Community 65"
 Cohesion: 1.0
 Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
 
-### Community 86 - "Community 86"
+### Community 77 - "Community 77"
 Cohesion: 0.67
 Nodes (1): updateUseModal()
 
-### Community 96 - "Community 96"
+### Community 87 - "Community 87"
 Cohesion: 1.0
 Nodes (2): buildMiniEncryptedLoginBody(), fetchLoginPublicKey()
 
-### Community 98 - "Community 98"
+### Community 89 - "Community 89"
 Cohesion: 0.67
 Nodes (1): return()
 
-### Community 152 - "Community 152"
+### Community 143 - "Community 143"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 153 - "Community 153"
+### Community 144 - "Community 144"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 154 - "Community 154"
+### Community 145 - "Community 145"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 155 - "Community 155"
+### Community 146 - "Community 146"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 156 - "Community 156"
+### Community 147 - "Community 147"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 157 - "Community 157"
+### Community 148 - "Community 148"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
-- **67 isolated node(s):** `关闭超时未支付的线上订单(超时分钟数来自系统设置 auto_cancel_minutes)。`, `初始化定时任务      Args:         get_db_session: 数据库会话获取函数(从 app.core.database 导入)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。`, `从请求体解析用户名、密码。     返回 (username, password, error_message);error_message 非空时表示失败。` (+62 more)
+- **71 isolated node(s):** `关闭超时未支付的线上订单(超时分钟数来自系统设置 auto_cancel_minutes)。`, `初始化定时任务      Args:         get_db_session: 数据库会话获取函数(从 app.core.database 导入)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。`, `从请求体解析用户名、密码。     返回 (username, password, error_message);error_message 非空时表示失败。` (+66 more)
   These have ≤1 connection - possible missing edges or undocumented components.
-- **Thin community `Community 33`** (13 nodes): `uni-combox.vue`, `clean()`, `data()`, `default()`, `filterCandidates()`, `filterCandidatesLength()`, `handler()`, `labelStyle()`, `onBlur()`, `onFocus()`, `onInput()`, `onSelectorClick()`, `toggleSelector()`
+- **Thin community `Community 24`** (13 nodes): `uni-combox.vue`, `clean()`, `data()`, `default()`, `filterCandidates()`, `filterCandidatesLength()`, `handler()`, `labelStyle()`, `onBlur()`, `onFocus()`, `onInput()`, `onSelectorClick()`, `toggleSelector()`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 31`** (9 nodes): `upload-file.vue`, `borderLineStyle()`, `borderStyle()`, `choose()`, `default()`, `delFile()`, `styles()`, `uploadFiles()`, `value2px()`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 33`** (8 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `setTransition()`, `setupEventListeners()`, `index.vue`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 34`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 42`** (9 nodes): `upload-file.vue`, `borderLineStyle()`, `borderStyle()`, `choose()`, `default()`, `delFile()`, `styles()`, `uploadFiles()`, `value2px()`
+- **Thin community `Community 36`** (8 nodes): `RouteValidator`, `.checkComponents()`, `.checkDuplicates()`, `.checkNestedIndexComponent()`, `.logLayoutError()`, `.resolvePath()`, `.validate()`, `RouteValidator.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 45`** (8 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `setTransition()`, `setupEventListeners()`, `index.vue`
+- **Thin community `Community 38`** (7 nodes): `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `App.vue`, `main.ts`, `console.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 46`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
+- **Thin community `Community 44`** (6 nodes): `bridge-error.js`, `BridgeError`, `.code()`, `.constructor()`, `.errCode()`, `.errMsg()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 54`** (6 nodes): `bridge-error.js`, `BridgeError`, `.code()`, `.constructor()`, `.errCode()`, `.errMsg()`
+- **Thin community `Community 46`** (6 nodes): `uni-thead.vue`, `checkboxSelected()`, `created()`, `data()`, `getTable()`, `init()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 56`** (6 nodes): `uni-thead.vue`, `checkboxSelected()`, `created()`, `data()`, `getTable()`, `init()`
+- **Thin community `Community 48`** (6 nodes): `uni-group.vue`, `created()`, `data()`, `getForm()`, `onClick()`, `title()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 58`** (6 nodes): `uni-group.vue`, `created()`, `data()`, `getForm()`, `onClick()`, `title()`
+- **Thin community `Community 51`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 61`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 55`** (4 nodes): `catch()`, `collectIds()`, `handleCategoryTableSelectionChange()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 71`** (4 nodes): `popup.js`, `created()`, `data()`, `getParent()`
+- **Thin community `Community 62`** (4 nodes): `popup.js`, `created()`, `data()`, `getParent()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 72`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 63`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 74`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
+- **Thin community `Community 65`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 86`** (3 nodes): `check-update.js`, `check-update.ts`, `updateUseModal()`
+- **Thin community `Community 77`** (3 nodes): `check-update.js`, `check-update.ts`, `updateUseModal()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 96`** (3 nodes): `loginRsa.js`, `buildMiniEncryptedLoginBody()`, `fetchLoginPublicKey()`
+- **Thin community `Community 87`** (3 nodes): `loginRsa.js`, `buildMiniEncryptedLoginBody()`, `fetchLoginPublicKey()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 98`** (3 nodes): `return()`, `detail.vue`, `detail.vue`
+- **Thin community `Community 89`** (3 nodes): `return()`, `detail.vue`, `detail.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 152`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 143`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 153`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 144`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 154`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
+- **Thin community `Community 145`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 155`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 146`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 156`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 147`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 157`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 148`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `push()` connect `Community 1` to `Community 3`, `Community 4`, `Community 5`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 17`?**
-  _High betweenness centrality (0.143) - this node is a cross-community bridge._
-- **Why does `query()` connect `Community 0` to `Community 64`, `Community 3`, `Community 6`, `Community 9`, `Community 43`, `Community 14`, `Community 16`, `Community 18`, `Community 23`?**
-  _High betweenness centrality (0.073) - this node is a cross-community bridge._
-- **Why does `number()` connect `Community 4` to `Community 1`, `Community 5`, `Community 9`, `Community 10`, `Community 13`, `Community 14`, `Community 17`, `Community 20`, `Community 29`?**
-  _High betweenness centrality (0.060) - this node is a cross-community bridge._
-- **Are the 338 inferred relationships involving `success_response()` (e.g. with `list_delivery_tasks()` and `get_delivery_task()`) actually correct?**
-  _`success_response()` has 338 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 259 inferred relationships involving `query()` (e.g. with `.check_delivery_warnings()` and `._region_advance_minutes()`) actually correct?**
-  _`query()` has 259 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 232 inferred relationships involving `str` (e.g. with `_run_delivery_warning_check()` and `_run_cleanup_expired_warnings()`) actually correct?**
-  _`str` has 232 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 235 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `require_admin()`) actually correct?**
-  _`error_response()` has 235 INFERRED edges - model-reasoned connections that need verification._
+- **Why does `push()` connect `Community 2` to `Community 0`, `Community 3`, `Community 6`, `Community 7`, `Community 8`, `Community 10`, `Community 11`?**
+  _High betweenness centrality (0.110) - this node is a cross-community bridge._
+- **Why does `query()` connect `Community 0` to `Community 16`, `Community 5`, `Community 54`, `Community 7`?**
+  _High betweenness centrality (0.050) - this node is a cross-community bridge._
+- **Why does `CRUDBase` connect `Community 5` to `Community 0`, `Community 4`?**
+  _High betweenness centrality (0.038) - this node is a cross-community bridge._
+- **Are the 539 inferred relationships involving `BizPriceListItem` (e.g. with `PriceListCRUD` and `PriceListItemCRUD`) actually correct?**
+  _`BizPriceListItem` has 539 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 532 inferred relationships involving `SysUser` (e.g. with `刷新会话 TTL(用于 refreshToken 延长会话)` and `写入 refreshToken(用于 one-time 使用/吊销)`) actually correct?**
+  _`SysUser` has 532 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 529 inferred relationships involving `BizOrder` (e.g. with `DeliveryTaskCRUD` and `DeliveryTaskItemCRUD`) actually correct?**
+  _`BizOrder` has 529 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 517 inferred relationships involving `BizDeliveryTask` (e.g. with `DeliveryWarningTask` and `配送时效预警定时任务  依据订单分配时写入的配送截止时间(biz_delivery_task.effective_time)及区域表 sys_region.`) actually correct?**
+  _`BizDeliveryTask` has 517 INFERRED edges - model-reasoned connections that need verification._

+ 1 - 0
graphify-out/cache/ast/03947cd05f5975be094042c91f56f0fe10d11f4fc5ae079608f9dda7e43d32da.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L569"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L569", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L819", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L570"}, {"caller_nid": "user_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L820"}]}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/05f42f7c6f3b7da70a11fa57a554a514543711642e12e1b85abe619cc6df38cd.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/064aae287ee25aa610bb95b26edfd2546415717022aa9e5f1e89304c8c012b90.json


+ 1 - 0
graphify-out/cache/ast/0836f9c9ec4fd76081af297798e5e5306ba79258fb9a8585fedcdbc2864afc11.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L473"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L473", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L668", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L474"}, {"caller_nid": "user_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L669"}]}

+ 1 - 0
graphify-out/cache/ast/08dee9727ab48d9f67f97a96e2e99988c97efb6a77e6145a108263aa69f9d9eb.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_components_core_layouts_art_menus_art_sidebar_menu_widget_sidebarsubmenu_vue", "label": "SidebarSubmenu.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/0d3bcd0ab87819a29c3b830347a91cdf4b74d91bb70024db556c2ca9b74cf61f.json


+ 1 - 0
graphify-out/cache/ast/0fc0f2b3fe9ec32bf063772f01243e2f13d914a103c383d6234050b79dff993f.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_delivery_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/delivery/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/104391934eedb1d272798974909ee1852ecc1c59736910b5df9fcafac78e349e.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/1188b91360c1eda95fca805e895436e6ecf2e99b1f383d6439f421a51c65d3fe.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/11b7d5bbb739da422c56eb5cea161d1d8669c8cbc017e05e9c4f743a62ca5d74.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/199204831a214bd3c1997062427253a0445835bac801eb9a0cc426feab17f023.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/1a4320188c67cca89f4df9a9cac739f92544e6611196a17c6c19707fefe8a008.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/1ab3a4687e0a158d6cf2b43d9f630d744551fb59ee5845beca7086b44ab16be3.json


+ 1 - 0
graphify-out/cache/ast/1b77c8772d3757c001bd8d191515c66f939bef47fc8468d73ba2b904c515a323.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_supplier_settlement_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/supplier-settlement/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/1bf8476c3a284f745f0c7f6ca0058a1a832600a4bc11a5ffcc513227acfa256d.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/1da2174c6e477538709721aae17956b56d8c23865ca8abc2e0bc3869ba3fef49.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/1eaeb2c94cd5780cbd5bd8b36341de1747147a5db72e6823a2cf7a00f58cf2f6.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/1fe590f88dbf3300c5dd8884ea8a665a6092c405f1d4fe5f63496959594148c9.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/204becf41bbbe95c690e7679fcc7446bb7dc06e40e0f74ad3a4e8ac06daad742.json


+ 1 - 0
graphify-out/cache/ast/2308aa152409b5985d16f30c90d27acd95c9dc918a8c66f58a5687388b1b742e.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_delivery_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/delivery/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/25a1c0c0fdf91536606ecf77e5ded3e56c70920fda3927933cb517d0dbca2abb.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_delivery_warning_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/delivery-warning/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/297ba8655b29b5bf2b70833e3b76c9fbe3fd69ba5e6d910a46a9ea199c6afdce.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/29ce30448874089741acbe1248299a30031349334470eb9ab7900540ce678cac.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/2e0fbcbccb2d6a78a15569a7b93c0d11bd6fb32cc2706b6de17553af053622b6.json


+ 1 - 0
graphify-out/cache/ast/2e213a8777799f206f600e762488136fbf7d19d636c3674d93889e8ccfa2f263.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/2e73adc434e154903a4ab163212346e4ccf564a9114de18cb076ccfb91fb4218.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L581"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L581", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L582"}]}

+ 1 - 0
graphify-out/cache/ast/2ead12d1f8729f7428e3bde66e4c2c6319bd5ee18cc7269795b6ed3f88382f86.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/300cfabaa2d74feb8bba359e0bb03832a3c5e01fa68076f0a6812e276cc703ba.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L569"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L569", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L819", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L570"}, {"caller_nid": "user_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L820"}]}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/3098b1bcaa73be883ce6597f633d7cf40f7bea2f16f8451ee502e3a3a6f2a3a8.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/33a32a68e0c3c3aa9d075743067fc4c87adfe496068b327661afb89d1e51a7fc.json


+ 1 - 0
graphify-out/cache/ast/33ae13cf79efefa6f01a364099f1237f42fa133cf3a4cc8b41faac11ddfff0c0.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_client_settlement_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/client-settlement/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/38bae257b39464ef6bfa567d418da9fd26c7829a1ce74a61a13bba31c26f6bd0.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_delivery_warning_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/delivery-warning/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/392feff2cd2311c81a95d30cdb370a53429f88d540ea0b95a4c6330a6c22d017.json


+ 1 - 0
graphify-out/cache/ast/3a65beebde0f3cc6b6ab18b63831bb25bb1a44f6cbae6641625c6506e1dc8f6e.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_router_modules_sqxp_ts", "label": "sqxp.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/router/modules/sqxp.ts", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_router_modules_sqxp_ts", "target": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_router", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/router/modules/sqxp.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/3b4a9fc2b951424b2ea706d12cb54595ea10c469fadf8a1f69a6eb4cb12c8527.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/3d3a445e2c77750e7f13827e6979e193deb731960a81b6f615f169607cb2e36b.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/3d826ad9e6fb66ea4ecfe8ea7fb853be5bbb60f446d2eb1c02ba0bcee9f12def.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/3e11fd520a3a26c0c4a2dd536633819bc5890b492a17edb1639aae60a41d0ec4.json


+ 1 - 0
graphify-out/cache/ast/40f833a6e47b6ec2a35bfb6af468891c95d221fcf547dc0aa169087d83d492f0.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_parameter_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/parameter/index.vue", "source_location": "L1"}, {"id": "parameter_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/parameter/index.vue", "source_location": "L759"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_parameter_index_vue", "target": "parameter_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/parameter/index.vue", "source_location": "L759", "weight": 1.0}], "raw_calls": [{"caller_nid": "parameter_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/parameter/index.vue", "source_location": "L760"}]}

+ 1 - 0
graphify-out/cache/ast/418a69a836e1bfb851db54a5cc13ce9930bc67696cad681232d69aa8a78343f1.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_delivery_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/delivery/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/41b1cb9bb73231c8bacfacd6ab60997e288886a21a53525fee652123d93edf77.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_supplier_settlement_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/supplier-settlement/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/429d76cf8147213dda2aad7d4981f9d318c28c83bf24246fecdc5b325f37e00d.json


+ 1 - 0
graphify-out/cache/ast/434a7cfb1a6720c6a0f9becd3cc8dc5e0d538cde508ad12ba737241cd729cfef.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/order/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/438644b3807df88a472822ca6a8703dd67f670d53a074e7201231b20edebf4d9.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/446418d6a7149f3ea7381a032965960d707b2a8e803abbae2bd0559b34150849.json


+ 1 - 0
graphify-out/cache/ast/463d240afaa41a831b6e42301d0c35dc74bc41937627e5ec0f76025210a85485.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_delivery_warning_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/delivery-warning/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/46c7ee7cfaae9757f71317d2e73eaf6cea5f32ccf8b1cf4da2f6723f3c8be70d.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/499dfe060c05d8115b40aa4e6e5850116b8cae496ddaf1c1353a14ad5a9f7ef0.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/55aab4c3dc11ea97004d32b3a374f62c4d6ff8b760e5ae4da65d070d679d1f57.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/55c5a5e4a7569058ee226b6aabefc816bd4d00e2eb14c35206c445d71f4540ec.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/5747e6ea6da5a17d1e9a9b5c83036c610d252820d371f6f2d74f3e8ed503b4df.json


+ 1 - 0
graphify-out/cache/ast/5880f90d93cd0833e2638431430eff436677c71b34d188284395ff544c992d63.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/order/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/58d2c3f9266a050dbba6d3563dc16944cd8c85cdcfe6b7ef46d55080db885a7a.json


+ 1 - 0
graphify-out/cache/ast/594c6571c4a873c4e548b000b9bbcb1d5680e840052f9deaac4aac0849aabe27.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_delivery_warning_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/delivery-warning/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/5b8be6a281267711bfe02769e371b7042cd6d9a6248fd50ca8b0f97f61c1d090.json


+ 1 - 0
graphify-out/cache/ast/65ffda25db0aae9c9fd644bc248e9c422b83c716a030c1e22349ffbb50191424.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L529"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L529", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L764", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L530"}, {"caller_nid": "user_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L765"}]}

+ 1 - 0
graphify-out/cache/ast/68500d939d59801336cbf5078ddae2fcdb6821948720d6df768a43ba997d14fd.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L581"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L581", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L901", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L582"}, {"caller_nid": "user_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L902"}]}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/6a24101e065e26af0d09fad5ddb85a060db628903c8f84b13ad700363dc1904f.json


+ 1 - 0
graphify-out/cache/ast/6a34e0fd99b3e805cf04416e43fba0c4c4c41ecd31243ad22435cc8188d26473.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L586"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L586", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L912", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L587"}, {"caller_nid": "user_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L913"}]}

+ 1 - 0
graphify-out/cache/ast/6b43dffcf38c4f82156febc9f7ef3950aa767ef112b32109a15c291dfdf187c1.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_delivery_warning_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/delivery-warning/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/6ee93c87fa8ba7de226bd0f1ce149beef9bba8bafca290e5c929ff20a000c661.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L529"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L529", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L756", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L530"}, {"caller_nid": "user_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L757"}]}

+ 1 - 0
graphify-out/cache/ast/701d99450dd7b094a81ec80c175f45f8b5c9b93781f6c74d74ec160eff7f9bee.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_client_settlement_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/client-settlement/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/717cf52372bb1442df1f5fc48ac47c818dc25d8e9ebbb7d1b2c5038d5cb2ffc5.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/order/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/722a5ceaa006d1a13a755ffe8b3a00d6dab791f715003d6f097bd9134eafe5ca.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/72de360d5f68a4930b7ce37be3ccdfa01550b8c40924506fc6a66a97b5623157.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/7308712a83c4f2141f72190a5d7df1b1038b237ac046c0a4559ded5349720b24.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/737e043d54f751035ae46cdccd41e37f405a523f8eafbb49cf3b7f3e98721d2d.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/754b8c35f364b3c7dce48e7c92e968b261d19c9613a1323e2fc94bf6bf37a97d.json


+ 1 - 0
graphify-out/cache/ast/75b7dde9a3ec52a106864d9199170d0581b9c35271f81d332e730020649d9fe9.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_client_settlement_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/client-settlement/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/75dea6c8de67695fa2c48f81247f7fa77d65352ae6b1b717f71c48248312ff85.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/76bbb5556baec19033c196c6453c5f4f1ed22b8b4bb6ce122d814f550fde3bb0.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/78c2314b5785565c880737cf199f00e8a03d19a2f55b598fe308bf222424f73a.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/7af904771af355220cc7065ee03ecdc865d86386c4b04f1e74f2de2b1245ce9a.json


+ 1 - 0
graphify-out/cache/ast/7b1b7d1b58b862ef2457958888a0a46a18b19e43c5c1465b0ceb8faa0824c2b5.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L1"}, {"id": "user_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L569"}], "edges": [{"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L569", "weight": 1.0}, {"source": "users_wuzilong_work_00code_sqxp_2_web_admin_src_views_system_user_index_vue", "target": "user_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L819", "weight": 1.0}], "raw_calls": [{"caller_nid": "user_index_if", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L570"}, {"caller_nid": "user_index_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/views/system/user/index.vue", "source_location": "L820"}]}

+ 1 - 0
graphify-out/cache/ast/7b2d775c8110b15a060457a318f01b54d7df90933b3d388e0cde73440e3c3b13.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
graphify-out/cache/ast/7bad736e3e79bb428518a9bcdf4e28266867dac6cc113e504793bc6e950bcc2f.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_sqxp_2_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/sqxp_\u526f\u672c2/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
graphify-out/cache/ast/7e6109fc138e14ba2354c1a58df48d07c7f76bca4fde8fcdceb28149ac6cd665.json


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است