'financial-operations/small-payment-work-orders', 'financial-ledger' => 'financial-operations/ledger', 'financial-ledger/ledger' => 'financial-operations/ledger', ]; private const FINAL_SECTION_KEYS = [ 'supplier-payables' => 'supplier-payables-final', 'courier-payables' => 'courier-payables-final', ]; public static function settings(): array { $value = setting('system.sidebar_permissions', []); if (is_string($value)) { $value = json_decode($value, true) ?: []; } return is_array($value) ? $value : []; } public static function subjectKey(User $user): string { $userKey = 'user_' . $user->id; $settings = self::settings(); if (in_array($userKey, $settings['configured'] ?? [], true)) { return $userKey; } return 'role_' . SystemAccess::roleName($user); } public static function allowedKeys(User $user): ?array { // Limited identities cannot inherit full navigation from roles, groups or user overrides. if (SupplierPortalAccess::isSupplierUser($user)) { return [SupplierPortalAccess::PAGE_KEY]; } // 非财务中心员工:只能看到财务经营风险管控下的两个页面。 if (self::hasRestrictedDingTalkAccess($user)) { return self::normalizeAllowedKeys((array) config('dingtalk.restricted_page_keys', [])); } if (self::hasLimitedDingTalkAccess($user)) { return config('dingtalk.limited_login_page_keys', []); } if (SystemAccess::isSuperAdmin($user)) { return null; } $settings = self::settings(); $userSubject = 'user_' . $user->id; if (in_array($userSubject, $settings['configured'] ?? [], true)) { return self::normalizeAllowedKeys((array) ($settings['subjects'][$userSubject] ?? [])); } $groups = UserAccessGroup::forUser($user); if ($groups->isNotEmpty()) { return self::normalizeAllowedKeys( $groups->flatMap(fn ($group) => $group->page_permissions ?? [])->all() ); } $subject = 'role_' . SystemAccess::roleName($user); if (! in_array($subject, $settings['configured'] ?? [], true)) { return null; } return self::normalizeAllowedKeys((array) ($settings['subjects'][$subject] ?? [])); } public static function hasLimitedDingTalkAccess(User $user): bool { return SupplierPortalAccess::isSupplierUser($user) || $user->created_from === 'dingtalk_roster_limited' || (config('dingtalk.limited_login_user_ids', []) !== [] && FinanceRoster::isLimitedLoginUser((string) $user->dingtalkIdentity?->dingtalk_user_id)); } public static function hasRestrictedDingTalkAccess(User $user): bool { return $user->created_from === 'dingtalk_restricted'; } public static function navigationFor(User $user): array { $navigation = BusinessNavigation::all(); $allowed = self::allowedKeys($user); if ($allowed === null) { return self::applyPageViewerAllowlists($navigation, $user); } // 费用明细承接原「广告费用 / 营销费用」入口,并复用账单明细的数据权限。 // 已保存过精确侧边栏权限的账号不会自动获得新页面键,因此按旧权限兼容显示。 $inheritsExpenseDetails = array_intersect([ 'financial-operations/performance-details', 'financial-operations/billing-details', 'financial-operations/advertising-expenses', 'financial-operations/marketing-expenses', ], $allowed) !== []; $navigation = array_values(array_filter(array_map(function (array $section) use ($allowed) { $sectionKey = $section['slug']; if (in_array($sectionKey, $allowed, true)) { return $section; } $pages = array_values(array_filter(array_map(function (array $page) use ($allowed, $sectionKey) { $pageKey = $sectionKey . '/' . $page['slug']; if (in_array($pageKey, $allowed, true)) { // 若有 children,保留 children 的精确权限过滤 if (! empty($page['children'])) { $page['children'] = array_values(array_filter($page['children'], function (array $child) use ($allowed, $sectionKey) { return in_array($sectionKey . '/' . $child['slug'], $allowed, true); })); } return $page; } // 子页面可能在 allowed 列表中(独立 URL),父级分组也允许展开 if (! empty($page['children'])) { $visibleChildren = array_values(array_filter($page['children'], function (array $child) use ($allowed, $sectionKey) { return in_array($sectionKey . '/' . $child['slug'], $allowed, true); })); if (! empty($visibleChildren)) { $page['children'] = $visibleChildren; return $page; } } return null; }, $section['pages'] ?? []))); if (! empty($pages)) { $section['pages'] = $pages; return $section; } return null; }, $navigation))); if ($inheritsExpenseDetails) { $configuredFinance = collect(BusinessNavigation::all()) ->firstWhere('slug', 'financial-operations'); $expensePage = collect($configuredFinance['pages'] ?? []) ->firstWhere('slug', 'expense-details'); $financeSectionFound = false; foreach ($navigation as &$financeSection) { if (($financeSection['slug'] ?? null) !== 'financial-operations') { continue; } $financeSectionFound = true; if (! $expensePage || collect($financeSection['pages'] ?? [])->contains('slug', 'expense-details')) { continue; } $pageSlugs = array_column($financeSection['pages'] ?? [], 'slug'); $performanceIndex = array_search('performance-details', $pageSlugs, true); $billingIndex = array_search('billing-details', $pageSlugs, true); $insertAt = is_int($performanceIndex) ? $performanceIndex + 1 : (is_int($billingIndex) ? $billingIndex : 0); array_splice($financeSection['pages'], $insertAt, 0, [$expensePage]); } unset($financeSection); // 只被授予旧费用入口的账号仍应保留这个替代页面。 if (! $financeSectionFound && $expensePage) { $fallbackSection = $configuredFinance; $fallbackSection['pages'] = [$expensePage]; $riskIndex = collect($navigation)->search( static fn (array $section): bool => ($section['slug'] ?? null) === 'financial-risk-control' ); array_splice( $navigation, is_int($riskIndex) ? $riskIndex : count($navigation), 0, [$fallbackSection], ); } } // 新增的公司成本安排、税金预测表与主体经营报表共用公司经营报表权限。 // 对已经保存过精确权限的账号兼容旧配置,避免新增页面被旧权限集合隐藏。 foreach ($navigation as &$section) { if (($section['slug'] ?? null) !== 'company-management') { continue; } $hasSubjectReport = collect($section['pages'] ?? [])->contains('slug', 'subject-operating-report'); if ($hasSubjectReport) { $configuredPages = collect(BusinessNavigation::all()) ->firstWhere('slug', 'company-management')['pages'] ?? []; foreach (['company-cost-arrangement', 'tax-forecast'] as $sharedReportSlug) { $alreadyVisible = collect($section['pages'] ?? [])->contains('slug', $sharedReportSlug); if ($alreadyVisible) { continue; } $sharedReportPage = collect($configuredPages)->firstWhere('slug', $sharedReportSlug); if ($sharedReportPage) { $section['pages'][] = $sharedReportPage; } } } } unset($section); // 「财务经营风险管控」新增「预览统计 / 流程统计」子页面:按页面键保存过精确权限的 // 账号只会有一条或两条,会看不到新页面;这里按「能看店铺违规预警 就能看新增页」 // 兼容旧配置(同属财务经营风险管控权限)。 foreach ($navigation as &$riskSection) { if (($riskSection['slug'] ?? null) !== 'financial-risk-control') { continue; } $pageSlugs = array_column($riskSection['pages'] ?? [], 'slug'); if (! in_array('store-violation-alert', $pageSlugs, true)) { continue; } $configuredPages = collect(BusinessNavigation::all()) ->firstWhere('slug', 'financial-risk-control')['pages'] ?? []; foreach ($configuredPages as $configuredPage) { $configuredSlug = (string) ($configuredPage['slug'] ?? ''); if (! in_array($configuredSlug, ['preview-statistics', 'process-statistics'], true)) { continue; } if (! in_array($configuredSlug, $pageSlugs, true)) { $riskSection['pages'][] = $configuredPage; } } } unset($riskSection); // 「种菜明细表 / 聚水潭订单标签明细」原先挂在「账单流水数据」下,后来移入新的一级菜单 // 「种菜数据汇总」。按页面键保存过精确权限的账号(访问组/角色)里不会有新的 planting-data, // 会因此看不到这两个被移动的页面;这里按「能看账单流水数据 就能看这两个页面」兼容旧配置。 // 「供应商对账」与「供应商应付(终版)」同属应付款项,先按同样的权限跟随授权 // (厂家账号后续按单页面单独配置)。 $visibleSlugs = array_column($navigation, 'slug'); if (! in_array('supplier-reconciliation', $visibleSlugs, true) && in_array('supplier-payables-final', $visibleSlugs, true)) { $reconciliationSection = collect(BusinessNavigation::all())->firstWhere('slug', 'supplier-reconciliation'); if (! empty($reconciliationSection)) { $index = array_search('supplier-payables-final', $visibleSlugs, true); array_splice($navigation, $index + 1, 0, [$reconciliationSection]); } } $slugs = array_column($navigation, 'slug'); if (! in_array('planting-data', $slugs, true) && in_array('billing-transactions', $slugs, true)) { $plantingSection = collect(BusinessNavigation::all())->firstWhere('slug', 'planting-data'); if (! empty($plantingSection)) { $billingIndex = array_search('billing-transactions', $slugs, true); array_splice($navigation, $billingIndex + 1, 0, [$plantingSection]); } } return self::applyPageViewerAllowlists($navigation, $user); } public static function canViewPage(User $user, string $section, string $page): bool { $pageKey = trim($section, '/') . '/' . trim($page, '/'); $allowedViewers = config('business_page_access.page_viewers')[$pageKey] ?? null; if (! is_array($allowedViewers)) { return true; } return in_array(trim((string) $user->name), $allowedViewers, true); } private static function applyPageViewerAllowlists(array $navigation, User $user): array { if (empty(config('business_page_access.page_viewers', []))) { return $navigation; } return array_values(array_filter(array_map( static function (array $section) use ($user): ?array { $sectionSlug = (string) ($section['slug'] ?? ''); $pages = $section['pages'] ?? []; if ($pages === []) { return $section; } $section['pages'] = array_values(array_filter( $pages, static fn (array $page): bool => self::canViewPage( $user, $sectionSlug, (string) ($page['slug'] ?? ''), ), )); return $section['pages'] === [] ? null : $section; }, $navigation, ))); } public static function allKeys(): array { $keys = []; foreach (BusinessNavigation::all() as $section) { $keys[] = $section['slug']; foreach ($section['pages'] ?? [] as $page) { $keys[] = $section['slug'] . '/' . $page['slug']; foreach ($page['children'] ?? [] as $child) { $keys[] = $section['slug'] . '/' . $child['slug']; } } } return $keys; } public static function normalizeAllowedKeys(array $keys): array { $normalized = []; foreach ($keys as $key) { $key = (string) $key; $normalizedKey = self::LEGACY_PAGE_KEYS[$key] ?? $key; $normalized[] = $normalizedKey; foreach (self::FINAL_SECTION_KEYS as $sectionKey => $finalSectionKey) { if ($normalizedKey === $sectionKey) { $normalized[] = $finalSectionKey; } elseif (str_starts_with($normalizedKey, $sectionKey . '/')) { // 「终版」目录已是独立目录,按同样的子页面粒度跟着授权。 $normalized[] = $finalSectionKey . substr($normalizedKey, strlen($sectionKey)); } } if ($key === 'refunds') { $normalized[] = self::LEGACY_PAGE_KEYS['refunds/small-payment-work-orders']; } } return array_values(array_unique($normalized)); } }