问题:管理员首页 Dashboard 显示平台总量(37 单,跨全部商户),点进「发货订单」 却只有 29 单,两个数字互相矛盾且界面上没有任何解释。 根因是「平台管理员」在两套接口里语义不同: - /api/dashboard:isPlatformAdmin 只看 JWT 角色 → 平台视角,跨商户聚合 - /api/merchant/*:Tenant 中间件强制落到单个 merchant_id → 商户成员视角。 管理员唯一的成员关系是自营商户,于是被静默框定在商户 1 且 34 条 /platform/* 路由中没有任何订单入口,前端也没有商户切换器或上下文提示。 修复(不改动商户侧隔离语义): - 新增 GET /api/platform/orders,support 按商户/状态/来源/关键字筛选,缺省为全部商户 - Service 抽出共用的 orderFilters 集中校验,ListOrders 与 ListPlatformOrders 并行; ListOrders 仍强制单商户,避免新增平台路径时顺手放宽商户侧可见范围 - FulfillmentOrder 增加 Merchant 关联字段,仅平台侧 Preload,商户侧响应结构不变 - 前端新增「下游订单(全部商户)」页与菜单项,「发货订单」更名为「发货订单(当前商户)」 - README 补充两个订单入口的说明,避免后来者重踩 验证:单测覆盖跨商户可见性、按商户筛选、筛选校验、以及商户侧隔离不回归; 实测新接口返回 37 单(自营 29 + 测试商户 8)且带商户信息,商户侧仍为 29 单且无 merchant 字段。
191 lines
7.1 KiB
TypeScript
191 lines
7.1 KiB
TypeScript
import { Suspense, lazy } from 'react'
|
|
import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
|
import { ConfigProvider, App as AntdApp, Spin } from 'antd'
|
|
import zhCN from 'antd/locale/zh_CN'
|
|
import { AuthProvider } from './store/auth'
|
|
import { useAuth } from './store/authContext'
|
|
import MainLayout from './layouts/MainLayout'
|
|
import Login from './pages/Login'
|
|
import type { ReactNode } from 'react'
|
|
|
|
// 路由级懒加载:把原先 1.6MB 的首屏单包按页面拆分。
|
|
// Login 与 MainLayout 保持同步加载——它们是未登录用户和登录后首帧的必经路径,
|
|
// 懒加载只会凭空多一次往返。
|
|
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
|
const OpenApiDocs = lazy(() => import('./pages/OpenApiDocs'))
|
|
const ApiDebugger = lazy(() => import('./pages/ApiDebugger'))
|
|
const Delivery = lazy(() => import('./pages/Delivery'))
|
|
const MerchantCenter = lazy(() => import('./pages/MerchantCenter'))
|
|
const MerchantRecharge = lazy(() => import('./pages/MerchantRecharge'))
|
|
const AlertSettings = lazy(() => import('./pages/AlertSettings'))
|
|
const PlatformMerchants = lazy(() => import('./pages/PlatformMerchants'))
|
|
const PlatformOrders = lazy(() => import('./pages/PlatformOrders'))
|
|
const PlatformProducts = lazy(() => import('./pages/PlatformProducts'))
|
|
const PlatformUsers = lazy(() => import('./pages/PlatformUsers'))
|
|
const PlatformRock = lazy(() => import('./pages/PlatformRock'))
|
|
|
|
// RouteFallback 是懒加载页面与权限校验的统一占位,避免切换时白屏。
|
|
function RouteFallback() {
|
|
return <div style={{ textAlign: 'center', padding: '100px 0' }}><Spin size="large" /></div>
|
|
}
|
|
|
|
function PrivateRoute({ children }: { children: ReactNode }) {
|
|
const { token } = useAuth()
|
|
if (!token) return <Navigate to="/login" replace />
|
|
return <>{children}</>
|
|
}
|
|
|
|
function AdminRoute({ children }: { children: ReactNode }) {
|
|
const { isAdmin } = useAuth()
|
|
if (!isAdmin) return <Navigate to="/" replace />
|
|
return <>{children}</>
|
|
}
|
|
|
|
const tabPermissions: Record<string, string> = {
|
|
products: 'products:manage',
|
|
orders: 'orders:manage',
|
|
wallet: 'wallet:ledger',
|
|
api: 'api:manage',
|
|
callbacks: 'callbacks:manage',
|
|
members: 'members:manage',
|
|
}
|
|
|
|
function MerchantPermissionRoute({ permission, children }: { permission?: string; children: ReactNode }) {
|
|
const { isAdmin, merchantLoading, merchantPermissions } = useAuth()
|
|
if (isAdmin || !permission) return <>{children}</>
|
|
if (merchantLoading) {
|
|
return <RouteFallback />
|
|
}
|
|
return merchantPermissions.includes(permission) ? <>{children}</> : <Navigate to="/" replace />
|
|
}
|
|
|
|
function MerchantCenterRoute() {
|
|
const location = useLocation()
|
|
const requestedTab = new URLSearchParams(location.search).get('tab') || 'products'
|
|
const tab = requestedTab in tabPermissions ? requestedTab : 'products'
|
|
return (
|
|
<MerchantPermissionRoute permission={tabPermissions[tab]}>
|
|
<MerchantCenter />
|
|
</MerchantPermissionRoute>
|
|
)
|
|
}
|
|
|
|
function AppRoutes() {
|
|
return (
|
|
<Routes>
|
|
<Route path="/login" element={<Login />} />
|
|
<Route path="/delivery/:channel/:orderNo" element={<Delivery />} />
|
|
<Route
|
|
path="/"
|
|
element={
|
|
<PrivateRoute>
|
|
<MainLayout />
|
|
</PrivateRoute>
|
|
}
|
|
>
|
|
<Route index element={<Dashboard />} />
|
|
<Route path="merchant-center" element={<MerchantCenterRoute />} />
|
|
<Route path="merchant-products" element={<MerchantPermissionRoute permission="products:manage"><MerchantCenter fixedTab="products" title="商品列表" /></MerchantPermissionRoute>} />
|
|
<Route path="merchant-orders" element={<MerchantPermissionRoute permission="orders:manage"><MerchantCenter fixedTab="orders" title="发货订单" /></MerchantPermissionRoute>} />
|
|
<Route path="merchant-wallet" element={<MerchantPermissionRoute permission="wallet:ledger"><MerchantCenter fixedTab="wallet" title="积分明细" /></MerchantPermissionRoute>} />
|
|
<Route path="merchant-recharge" element={<MerchantPermissionRoute permission="wallet:view"><MerchantRecharge /></MerchantPermissionRoute>} />
|
|
<Route path="merchant-alert-settings" element={<MerchantPermissionRoute permission="recharge:manage"><AlertSettings /></MerchantPermissionRoute>} />
|
|
<Route path="merchant-members" element={<MerchantPermissionRoute permission="members:manage"><MerchantCenter fixedTab="members" title="成员" /></MerchantPermissionRoute>} />
|
|
<Route path="merchant-callbacks" element={<MerchantPermissionRoute permission="callbacks:manage"><MerchantCenter fixedTab="callbacks" title="回调" /></MerchantPermissionRoute>} />
|
|
<Route path="merchant-api-keys" element={<MerchantPermissionRoute permission="api:manage"><MerchantCenter fixedTab="api" title="API 密钥" /></MerchantPermissionRoute>} />
|
|
<Route
|
|
path="platform-orders"
|
|
element={
|
|
<AdminRoute>
|
|
<PlatformOrders />
|
|
</AdminRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="platform-merchants"
|
|
element={
|
|
<AdminRoute>
|
|
<PlatformMerchants />
|
|
</AdminRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="platform-products"
|
|
element={
|
|
<AdminRoute>
|
|
<PlatformProducts />
|
|
</AdminRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="platform-rock"
|
|
element={
|
|
<AdminRoute>
|
|
<PlatformRock />
|
|
</AdminRoute>
|
|
}
|
|
/>
|
|
<Route path="platform-users" element={<AdminRoute><PlatformUsers /></AdminRoute>} />
|
|
<Route path="open-api" element={<MerchantPermissionRoute permission="api:manage"><OpenApiDocs /></MerchantPermissionRoute>} />
|
|
<Route
|
|
path="api-debug"
|
|
element={
|
|
<AdminRoute>
|
|
<ApiDebugger />
|
|
</AdminRoute>
|
|
}
|
|
/>
|
|
</Route>
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
)
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<ConfigProvider
|
|
locale={zhCN}
|
|
theme={{
|
|
token: {
|
|
colorPrimary: '#2563eb',
|
|
colorInfo: '#2563eb',
|
|
colorSuccess: '#16a34a',
|
|
colorWarning: '#d97706',
|
|
colorError: '#dc2626',
|
|
borderRadius: 8,
|
|
fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif`,
|
|
},
|
|
components: {
|
|
Card: {
|
|
paddingLG: 20,
|
|
borderRadiusLG: 10,
|
|
},
|
|
Table: {
|
|
headerBg: '#f8fafc',
|
|
headerColor: '#475569',
|
|
headerSplitColor: 'transparent',
|
|
rowHoverBg: '#f1f5f9',
|
|
},
|
|
Button: {
|
|
fontWeight: 600,
|
|
borderRadius: 6,
|
|
},
|
|
Tag: {
|
|
borderRadiusSM: 4,
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<AntdApp>
|
|
<AuthProvider>
|
|
<BrowserRouter>
|
|
<Suspense fallback={<RouteFallback />}>
|
|
<AppRoutes />
|
|
</Suspense>
|
|
</BrowserRouter>
|
|
</AuthProvider>
|
|
</AntdApp>
|
|
</ConfigProvider>
|
|
)
|
|
}
|