From c6b695169e1cbbeed147cff3b81279dad9c80e1c Mon Sep 17 00:00:00 2001 From: kangwenjing <1138819403@qq.com> Date: Wed, 1 Jul 2026 09:11:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9D=83=E9=99=90=E7=82=B9?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../17-7b32e674f7a9424f9e8de455d34b451c.zip | 1 + .../service/impl/ExpansionServiceImpl.java | 18 +- .../service/impl/OpportunityServiceImpl.java | 15 +- .../impl/ExpansionServiceImplTest.java | 33 +++ .../impl/OpportunityServiceImplTest.java | 19 ++ frontend/src/lib/auth.ts | 36 ++- frontend/src/pages/Expansion.tsx | 48 ++- frontend/src/pages/Opportunities.tsx | 48 ++- frontend1/dist/index.html | 2 +- frontend1/src/layouts/AppLayout.tsx | 7 + ...60630_expansion_create_permission_pg17.sql | 203 +++++++++++++ ...630_opportunity_create_permission_pg17.sql | 278 ++++++++++++++++++ 12 files changed, 690 insertions(+), 18 deletions(-) create mode 100644 backend/build/test-uploads/work-report-attachments/17-7b32e674f7a9424f9e8de455d34b451c.zip create mode 100644 sql/20260630_expansion_create_permission_pg17.sql create mode 100644 sql/20260630_opportunity_create_permission_pg17.sql diff --git a/backend/build/test-uploads/work-report-attachments/17-7b32e674f7a9424f9e8de455d34b451c.zip b/backend/build/test-uploads/work-report-attachments/17-7b32e674f7a9424f9e8de455d34b451c.zip new file mode 100644 index 00000000..82090ee2 --- /dev/null +++ b/backend/build/test-uploads/work-report-attachments/17-7b32e674f7a9424f9e8de455d34b451c.zip @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java index 50249932..4578f74a 100644 --- a/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java +++ b/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java @@ -1,6 +1,7 @@ package com.unis.crm.service.impl; import com.unis.crm.common.BusinessException; +import com.unis.crm.common.UnauthorizedException; import com.unis.crm.dto.expansion.ChannelExpansionItemDTO; import com.unis.crm.dto.expansion.ChannelExpansionContactRequest; import com.unis.crm.dto.expansion.ChannelRelatedProjectSummaryDTO; @@ -18,6 +19,7 @@ import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest; import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest; import com.unis.crm.mapper.ExpansionMapper; import com.unis.crm.service.ExpansionService; +import com.unisbase.security.PermissionService; import com.unisbase.security.SpringSecurityTenantProvider; import java.math.BigDecimal; import java.net.URLDecoder; @@ -47,16 +49,22 @@ public class ExpansionServiceImpl implements ExpansionService { private static final String CERTIFICATION_LEVEL_TYPE_CODE = "tz_rzjb"; private static final String CHANNEL_ATTRIBUTE_TYPE_CODE = "tz_qdsx"; private static final String INTERNAL_ATTRIBUTE_TYPE_CODE = "tz_xhsnbsx"; + private static final String CREATE_PERMISSION = "expansion:create"; private static final String MULTI_VALUE_CUSTOM_PREFIX = "__custom__:"; private static final DateTimeFormatter FOLLOW_UP_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private static final Logger log = LoggerFactory.getLogger(ExpansionServiceImpl.class); private final ExpansionMapper expansionMapper; private final SpringSecurityTenantProvider tenantProvider; + private final PermissionService permissionService; - public ExpansionServiceImpl(ExpansionMapper expansionMapper, SpringSecurityTenantProvider tenantProvider) { + public ExpansionServiceImpl( + ExpansionMapper expansionMapper, + SpringSecurityTenantProvider tenantProvider, + PermissionService permissionService) { this.expansionMapper = expansionMapper; this.tenantProvider = tenantProvider; + this.permissionService = permissionService; } @Override @@ -148,6 +156,7 @@ public class ExpansionServiceImpl implements ExpansionService { @Override public Long createSalesExpansion(Long userId, CreateSalesExpansionRequest request) { + requirePermission(CREATE_PERMISSION, "无权新增拓展"); fillSalesDefaults(request); ensureUniqueEmployeeNo(userId, request.getEmployeeNo(), null); expansionMapper.insertSalesExpansion(userId, request); @@ -160,6 +169,7 @@ public class ExpansionServiceImpl implements ExpansionService { @Override @Transactional public Long createChannelExpansion(Long userId, CreateChannelExpansionRequest request) { + requirePermission(CREATE_PERMISSION, "无权新增拓展"); fillChannelDefaults(request); ensureUniqueChannelName(userId, request.getChannelName(), null); expansionMapper.insertChannelExpansion(userId, request); @@ -675,4 +685,10 @@ public class ExpansionServiceImpl implements ExpansionService { throw new BusinessException("无权操作该拓展记录"); } } + + private void requirePermission(String perm, String message) { + if (!permissionService.hasPermi(perm)) { + throw new UnauthorizedException(message); + } + } } diff --git a/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java index 0a1525ca..819acce4 100644 --- a/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java +++ b/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.unis.crm.common.BusinessException; +import com.unis.crm.common.UnauthorizedException; import com.unis.crm.dto.opportunity.CurrentUserAccountDTO; import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest; import com.unis.crm.dto.opportunity.CreateOpportunityRequest; @@ -23,6 +24,7 @@ import com.unis.crm.mapper.OpportunityMapper; import com.unis.crm.service.OmsClient; import com.unis.crm.service.OpportunityService; import com.unisbase.dto.DataScopeRuleDTO; +import com.unisbase.security.PermissionService; import com.unisbase.service.DataScopeService; import com.unisbase.spi.UnisBaseTenantProvider; import java.io.IOException; @@ -50,6 +52,7 @@ public class OpportunityServiceImpl implements OpportunityService { private static final String OPPORTUNITY_TYPE_CODE = "sj_jslx"; private static final String CONFIDENCE_TYPE_CODE = "sj_xmbwd"; private static final String SYS_IS_TYPE_CODE = "sys_is"; + private static final String CREATE_PERMISSION = "opportunity:create"; private static final String ARCHIVED_OPPORTUNITY_EDIT_MESSAGE = "已签单商机不允许编辑"; private static final String ARCHIVED_OPPORTUNITY_PUSH_MESSAGE = "已签单商机不允许推送"; private static final String REPORT_ATTACHMENT_METADATA_PREFIX = "[[WORK_REPORT_ATTACHMENTS]]"; @@ -62,18 +65,21 @@ public class OpportunityServiceImpl implements OpportunityService { private final ObjectMapper objectMapper; private final DataScopeService dataScopeService; private final UnisBaseTenantProvider tenantProvider; + private final PermissionService permissionService; public OpportunityServiceImpl( OpportunityMapper opportunityMapper, OmsClient omsClient, ObjectMapper objectMapper, DataScopeService dataScopeService, - UnisBaseTenantProvider tenantProvider) { + UnisBaseTenantProvider tenantProvider, + PermissionService permissionService) { this.opportunityMapper = opportunityMapper; this.omsClient = omsClient; this.objectMapper = objectMapper; this.dataScopeService = dataScopeService; this.tenantProvider = tenantProvider; + this.permissionService = permissionService; } @Override @@ -169,6 +175,7 @@ public class OpportunityServiceImpl implements OpportunityService { @Override @Transactional public Long createOpportunity(Long userId, CreateOpportunityRequest request) { + requirePermission(CREATE_PERMISSION, "无权新增商机"); fillDefaults(request); Long customerId = resolveOrCreateOwnedCustomerId(userId, request.getCustomerName().trim(), request.getSource()); @@ -997,6 +1004,12 @@ public class OpportunityServiceImpl implements OpportunityService { || normalizedOperator.contains("dls"); } + private void requirePermission(String perm, String message) { + if (!permissionService.hasPermi(perm)) { + throw new UnauthorizedException(message); + } + } + private OmsPreSalesOptionDTO resolveSelectedPreSales( OpportunityOmsPushDataDTO pushData, PushOpportunityToOmsRequest request, diff --git a/backend/src/test/java/com/unis/crm/service/impl/ExpansionServiceImplTest.java b/backend/src/test/java/com/unis/crm/service/impl/ExpansionServiceImplTest.java index 12608037..83ea7b6f 100644 --- a/backend/src/test/java/com/unis/crm/service/impl/ExpansionServiceImplTest.java +++ b/backend/src/test/java/com/unis/crm/service/impl/ExpansionServiceImplTest.java @@ -9,6 +9,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.unis.crm.common.BusinessException; +import com.unis.crm.common.UnauthorizedException; import com.unis.crm.dto.expansion.ChannelExpansionContactRequest; import com.unis.crm.dto.expansion.CreateChannelExpansionRequest; import com.unis.crm.dto.expansion.CreateSalesExpansionRequest; @@ -17,6 +18,7 @@ import com.unis.crm.dto.expansion.SalesExpansionItemDTO; import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest; import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest; import com.unis.crm.mapper.ExpansionMapper; +import com.unisbase.security.PermissionService; import com.unisbase.security.SpringSecurityTenantProvider; import java.math.BigDecimal; import java.time.LocalDate; @@ -36,12 +38,16 @@ class ExpansionServiceImplTest { @Mock private SpringSecurityTenantProvider tenantProvider; + @Mock + private PermissionService permissionService; + @InjectMocks private ExpansionServiceImpl expansionService; @Test void createSalesExpansion_shouldRejectDuplicateEmployeeNo() { CreateSalesExpansionRequest request = buildCreateSalesRequest(); + when(permissionService.hasPermi("expansion:create")).thenReturn(true); when(expansionMapper.countSalesExpansionByEmployeeNo("EMP001")).thenReturn(1); BusinessException exception = assertThrows( @@ -52,6 +58,19 @@ class ExpansionServiceImplTest { verify(expansionMapper, never()).insertSalesExpansion(any(), any()); } + @Test + void createSalesExpansion_shouldRejectWhenPermissionMissing() { + CreateSalesExpansionRequest request = buildCreateSalesRequest(); + when(permissionService.hasPermi("expansion:create")).thenReturn(false); + + UnauthorizedException exception = assertThrows( + UnauthorizedException.class, + () -> expansionService.createSalesExpansion(1L, request)); + + assertEquals("无权新增拓展", exception.getMessage()); + verify(expansionMapper, never()).insertSalesExpansion(any(), any()); + } + @Test void updateSalesExpansion_shouldSkipDuplicateEmployeeNoCheck() { UpdateSalesExpansionRequest request = buildUpdateSalesRequest(); @@ -67,6 +86,7 @@ class ExpansionServiceImplTest { @Test void createChannelExpansion_shouldRejectDuplicateChannelName() { CreateChannelExpansionRequest request = buildCreateChannelRequest(); + when(permissionService.hasPermi("expansion:create")).thenReturn(true); when(expansionMapper.countChannelExpansionByChannelName("渠道A")).thenReturn(1); BusinessException exception = assertThrows( @@ -77,6 +97,19 @@ class ExpansionServiceImplTest { verify(expansionMapper, never()).insertChannelExpansion(any(), any()); } + @Test + void createChannelExpansion_shouldRejectWhenPermissionMissing() { + CreateChannelExpansionRequest request = buildCreateChannelRequest(); + when(permissionService.hasPermi("expansion:create")).thenReturn(false); + + UnauthorizedException exception = assertThrows( + UnauthorizedException.class, + () -> expansionService.createChannelExpansion(1L, request)); + + assertEquals("无权新增拓展", exception.getMessage()); + verify(expansionMapper, never()).insertChannelExpansion(any(), any()); + } + @Test void updateChannelExpansion_shouldSkipDuplicateChannelNameCheck() { UpdateChannelExpansionRequest request = buildUpdateChannelRequest(); diff --git a/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java b/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java index ea3f71b0..a44e7a81 100644 --- a/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java +++ b/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; import com.unis.crm.common.BusinessException; +import com.unis.crm.common.UnauthorizedException; import com.unis.crm.dto.opportunity.CurrentUserAccountDTO; import com.unis.crm.dto.opportunity.CreateOpportunityRequest; import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO; @@ -24,6 +25,7 @@ import com.unis.crm.dto.opportunity.UpdateOpportunityIntegrationRequest; import com.unis.crm.mapper.OpportunityMapper; import com.unis.crm.service.OmsClient; import com.unisbase.dto.DataScopeRuleDTO; +import com.unisbase.security.PermissionService; import com.unisbase.service.DataScopeService; import com.unisbase.spi.UnisBaseTenantProvider; import java.math.BigDecimal; @@ -53,6 +55,9 @@ class OpportunityServiceImplTest { @Mock private UnisBaseTenantProvider tenantProvider; + @Mock + private PermissionService permissionService; + @InjectMocks private OpportunityServiceImpl opportunityService; @@ -84,6 +89,7 @@ class OpportunityServiceImplTest { @Test void createOpportunity_shouldRejectWhenCustomerDoesNotExist() { + when(permissionService.hasPermi("opportunity:create")).thenReturn(true); when(opportunityMapper.selectOwnedCustomerIdByName(1L, "客户A")).thenReturn(null); when(opportunityMapper.insertOpportunity(eq(1L), any(), any(CreateOpportunityRequest.class))).thenAnswer(invocation -> { CreateOpportunityRequest request = invocation.getArgument(2); @@ -102,6 +108,18 @@ class OpportunityServiceImplTest { verify(opportunityMapper).insertCustomer(any(), eq(1L), eq("客户A"), any()); } + @Test + void createOpportunity_shouldRejectWhenPermissionMissing() { + when(permissionService.hasPermi("opportunity:create")).thenReturn(false); + + UnauthorizedException exception = assertThrows( + UnauthorizedException.class, + () -> opportunityService.createOpportunity(1L, updateRequest("客户A"))); + + assertEquals("无权新增商机", exception.getMessage()); + verify(opportunityMapper, never()).insertOpportunity(any(), any(), any()); + } + @Test void updateOpportunity_shouldKeepCurrentCustomerWhenCustomerNameUnchanged() { when(opportunityMapper.countOwnedOpportunity(1L, 10L)).thenReturn(1); @@ -172,6 +190,7 @@ class OpportunityServiceImplTest { @Test void createOpportunity_shouldPersistEditableSnapshotFields() { + when(permissionService.hasPermi("opportunity:create")).thenReturn(true); when(opportunityMapper.selectOwnedCustomerIdByName(1L, "客户A")).thenReturn(200L); when(opportunityMapper.insertOpportunity(eq(1L), eq(200L), any(CreateOpportunityRequest.class))).thenAnswer(invocation -> { CreateOpportunityRequest request = invocation.getArgument(2); diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 98a3aa3d..89255037 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -46,6 +46,16 @@ export interface UserRole { roleName?: string; } +export interface UserPermission { + permId?: number; + parentId?: number; + name?: string; + code: string; + permType?: string; + status?: number; + isVisible?: number; +} + export interface AdminUserSummary { userId: number; username: string; @@ -808,6 +818,7 @@ const LOGIN_PATH = "/login"; const USER_CONTEXT_CACHE_TTL_MS = 2 * 60 * 1000; const CURRENT_USER_CACHE_KEY = "auth-cache:current-user"; const PROFILE_OVERVIEW_CACHE_KEY = "auth-cache:profile-overview"; +const MY_PERMISSIONS_CACHE_KEY = "auth-cache:my-permissions"; const memoryRequestCache = new Map(); const inFlightRequestCache = new Map>(); let authExpiryTimer: number | null = null; @@ -1235,6 +1246,29 @@ export async function refreshCurrentUser() { return profile; } +export async function listMyPermissions() { + return getCachedAuthedRequest( + MY_PERMISSIONS_CACHE_KEY, + () => request("/api/sys/api/permissions/me", undefined, true), + ); +} + +export function canUsePermission(perm: string, codes: string[]) { + if (!perm) { + return true; + } + if (!codes.length) { + return true; + } + + const hasMenuCodes = codes.some((code) => code.startsWith("menu:")); + const hasButtonCodes = codes.some((code) => !code.startsWith("menu:") && code.includes(":")); + if (perm.startsWith("menu:")) { + return !hasMenuCodes || codes.includes(perm); + } + return !hasButtonCodes || codes.includes(perm); +} + export async function getDashboardHome() { return request("/api/dashboard/home", undefined, true); } @@ -1650,7 +1684,7 @@ function writeCachedValue(cacheKey: string, value: T, ttlMs = USER_CONTEXT_CA } function clearCachedAuthContext() { - [CURRENT_USER_CACHE_KEY, PROFILE_OVERVIEW_CACHE_KEY].forEach((cacheKey) => { + [CURRENT_USER_CACHE_KEY, PROFILE_OVERVIEW_CACHE_KEY, MY_PERMISSIONS_CACHE_KEY].forEach((cacheKey) => { memoryRequestCache.delete(cacheKey); inFlightRequestCache.delete(cacheKey); try { diff --git a/frontend/src/pages/Expansion.tsx b/frontend/src/pages/Expansion.tsx index 1ff34379..005586d5 100644 --- a/frontend/src/pages/Expansion.tsx +++ b/frontend/src/pages/Expansion.tsx @@ -3,6 +3,7 @@ import { Search, Plus, Download, MapPin, Building2, User, Phone, X, Clock, FileT import { motion, AnimatePresence } from "motion/react"; import { useLocation } from "react-router-dom"; import { + canUsePermission, checkChannelExpansionDuplicate, checkSalesExpansionDuplicate, createChannelExpansion, @@ -13,6 +14,7 @@ import { getExpansionOverview, getOpportunityMeta, getStoredCurrentUserId, + listMyPermissions, updateChannelExpansion, updateSalesExpansion, type ChannelExpansionContact, @@ -138,6 +140,7 @@ function createEmptyChannelContact(): ChannelExpansionContact { } const CHANNEL_REVENUE_LABEL = "年度营业额(万元)"; +const EXPANSION_CREATE_PERMISSION = "expansion:create"; const EXPANSION_EXPORT_PREFERENCES_STORAGE_KEY = "crm:expansion-export-preferences"; const defaultSalesForm: CreateSalesExpansionPayload = { @@ -1353,6 +1356,7 @@ export default function Expansion() { const [exporting, setExporting] = useState(false); const [exportFilterOpen, setExportFilterOpen] = useState(false); const [exportFilters, setExportFilters] = useState(() => loadExpansionExportPreferences()); + const [permissionCodes, setPermissionCodes] = useState(null); const [salesDuplicateChecking, setSalesDuplicateChecking] = useState(false); const [channelDuplicateChecking, setChannelDuplicateChecking] = useState(false); const [createError, setCreateError] = useState(""); @@ -1375,6 +1379,7 @@ export default function Expansion() { const [editSalesForm, setEditSalesForm] = useState(defaultSalesForm); const [editChannelForm, setEditChannelForm] = useState(defaultChannelForm); const hasForegroundModal = createOpen || editOpen || exportFilterOpen; + const canCreateExpansion = permissionCodes !== null && canUsePermission(EXPANSION_CREATE_PERMISSION, permissionCodes); const loadMeta = useCallback(async () => { const data = await getExpansionMeta(); @@ -1388,6 +1393,33 @@ export default function Expansion() { return data; }, []); + useEffect(() => { + let cancelled = false; + + async function loadPermissions() { + try { + const permissions = await listMyPermissions(); + if (!cancelled) { + setPermissionCodes( + permissions + .filter((permission) => permission.status !== 0) + .map((permission) => permission.code) + .filter(Boolean), + ); + } + } catch { + if (!cancelled) { + setPermissionCodes(null); + } + } + } + + void loadPermissions(); + return () => { + cancelled = true; + }; + }, []); + useEffect(() => { let cancelled = false; @@ -2516,13 +2548,15 @@ export default function Expansion() { {exporting ? "导出中..." : "导出"} - + {canCreateExpansion ? ( + + ) : null} diff --git a/frontend/src/pages/Opportunities.tsx b/frontend/src/pages/Opportunities.tsx index 3ace4902..6f69ba7a 100644 --- a/frontend/src/pages/Opportunities.tsx +++ b/frontend/src/pages/Opportunities.tsx @@ -6,6 +6,7 @@ import { useLocation } from "react-router-dom"; import { checkChannelExpansionDuplicate, checkSalesExpansionDuplicate, + canUsePermission, createChannelExpansion, createOpportunity, createSalesExpansion, @@ -17,6 +18,7 @@ import { getOpportunityOmsPreSalesOptions, getOpportunityOverview, getStoredCurrentUserId, + listMyPermissions, pushOpportunityToOms, updateOpportunity, type ChannelExpansionContact, @@ -68,6 +70,7 @@ const OPPORTUNITY_EXPANSION_OPTION_LIMIT = 20; const OPPORTUNITY_EXPANSION_SEARCH_DEBOUNCE_MS = 300; const OPPORTUNITY_NEXT_PLAN_LABEL = "下一步销售计划"; const LEGACY_OPPORTUNITY_NEXT_PLAN_LABEL = "后续规划"; +const OPPORTUNITY_CREATE_PERMISSION = "opportunity:create"; const OPPORTUNITY_EXPORT_PREFERENCES_STORAGE_KEY = "crm:opportunity-export-preferences"; const COMPETITOR_OPTIONS = [ @@ -1854,6 +1857,7 @@ export default function Opportunities() { const [exporting, setExporting] = useState(false); const [exportFilterOpen, setExportFilterOpen] = useState(false); const [exportFilters, setExportFilters] = useState(() => loadOpportunityExportPreferences()); + const [permissionCodes, setPermissionCodes] = useState(null); const [error, setError] = useState(""); const [exportError, setExportError] = useState(""); const [items, setItems] = useState([]); @@ -1898,12 +1902,40 @@ export default function Opportunities() { const [loadingChannelExpansionOptions, setLoadingChannelExpansionOptions] = useState(false); const hasLoadedOpportunityExpansionOptionsRef = useRef(false); const hasForegroundModal = createOpen || editOpen || pushConfirmOpen || exportFilterOpen || quickCreateOpen; + const canCreateOpportunity = permissionCodes !== null && canUsePermission(OPPORTUNITY_CREATE_PERMISSION, permissionCodes); const fetchOpportunityExpansionOptions = async (query?: string) => getOpportunityExpansionOptions({ keyword: query, limit: OPPORTUNITY_EXPANSION_OPTION_LIMIT, }); + useEffect(() => { + let cancelled = false; + + async function loadPermissions() { + try { + const permissions = await listMyPermissions(); + if (!cancelled) { + setPermissionCodes( + permissions + .filter((permission) => permission.status !== 0) + .map((permission) => permission.code) + .filter(Boolean), + ); + } + } catch { + if (!cancelled) { + setPermissionCodes(null); + } + } + } + + void loadPermissions(); + return () => { + cancelled = true; + }; + }, []); + useEffect(() => { let cancelled = false; @@ -2867,13 +2899,15 @@ export default function Opportunities() { {exporting ? "导出中..." : "导出"} - + {canCreateOpportunity ? ( + + ) : null} diff --git a/frontend1/dist/index.html b/frontend1/dist/index.html index 521d1e7d..3dd8ea4d 100644 --- a/frontend1/dist/index.html +++ b/frontend1/dist/index.html @@ -5,7 +5,7 @@ UnisBase - 智能会议系统 - + diff --git a/frontend1/src/layouts/AppLayout.tsx b/frontend1/src/layouts/AppLayout.tsx index dcf6f6f2..9521eda3 100644 --- a/frontend1/src/layouts/AppLayout.tsx +++ b/frontend1/src/layouts/AppLayout.tsx @@ -62,6 +62,12 @@ function isPlatformOnlyMenu(item: SysPermission) { || item.path === "/platform-settings"; } +function isPermissionOnlyMenu(item: SysPermission) { + return item.code === "crm" + || item.code === "menu:expansion" + || item.code === "menu:opportunities"; +} + function resolveSelectedMenuPath(pathname: string): string { if (pathname.startsWith("/dashboard-analytics-settings/cards/")) { return "/dashboard-analytics-settings"; @@ -143,6 +149,7 @@ export default function AppLayout() { const filtered = data .filter((item) => isPlatformMode || !isPlatformOnlyMenu(item)) + .filter((item) => !isPermissionOnlyMenu(item)) .filter((item) => (item.permType === "menu" || item.permType === "directory") && item.isVisible === 1 && item.status === 1) .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); setMenus(filtered); diff --git a/sql/20260630_expansion_create_permission_pg17.sql b/sql/20260630_expansion_create_permission_pg17.sql new file mode 100644 index 00000000..378ef71d --- /dev/null +++ b/sql/20260630_expansion_create_permission_pg17.sql @@ -0,0 +1,203 @@ +-- 20260630 +-- 修复/补齐前台拓展管理“新增”按钮权限点:expansion:create +-- 执行后可在 frontend1 角色管理的功能权限树中看到:CRM业务 / 拓展 / 新增拓展。 +-- 可重复执行;CRM业务与拓展节点仅用于权限管理,不在后台侧边栏显示。 + +begin; + +set search_path to public; + +do $$ +declare + v_business_parent_id bigint; + v_expansion_menu_perm_id bigint; + v_expansion_create_perm_id bigint; + v_has_role_permission_tenant boolean; +begin + perform setval('sys_permission_perm_id_seq', coalesce((select max(perm_id) from sys_permission), 0) + 1, false); + perform setval('sys_role_permission_id_seq', coalesce((select max(id) from sys_role_permission), 0) + 1, false); + + select exists ( + select 1 + from information_schema.columns + where table_schema = current_schema() + and table_name = 'sys_role_permission' + and column_name = 'tenant_id' + ) into v_has_role_permission_tenant; + + select perm_id + into v_business_parent_id + from sys_permission + where code = 'crm' + order by perm_id + limit 1; + + if v_business_parent_id is null then + insert into sys_permission ( + parent_id, name, code, perm_type, level, path, component, icon, + sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at + ) values ( + null, 'CRM业务', 'crm', 'directory', 1, null, null, 'AppstoreOutlined', + 10, 0, 1, 'CRM前台业务权限分组,仅用于权限管理,不在后台侧边栏显示', '{}'::jsonb, 0, now(), now() + ) + returning perm_id into v_business_parent_id; + else + update sys_permission + set name = 'CRM业务', + perm_type = 'directory', + level = 1, + path = null, + component = null, + icon = coalesce(nullif(icon, ''), 'AppstoreOutlined'), + sort_order = coalesce(sort_order, 10), + is_visible = 0, + status = 1, + description = 'CRM前台业务权限分组,仅用于权限管理,不在后台侧边栏显示', + meta = coalesce(meta, '{}'::jsonb), + is_deleted = 0, + updated_at = now() + where perm_id = v_business_parent_id; + end if; + + select perm_id + into v_expansion_menu_perm_id + from sys_permission + where code = 'menu:expansion' + order by perm_id + limit 1; + + if v_expansion_menu_perm_id is null then + insert into sys_permission ( + parent_id, name, code, perm_type, level, path, component, icon, + sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at + ) values ( + v_business_parent_id, '拓展', 'menu:expansion', 'menu', 2, + null, null, 'TeamOutlined', 20, 0, 1, + 'CRM前台拓展管理页面权限分组,仅用于权限管理,不在后台侧边栏显示', '{}'::jsonb, 0, now(), now() + ) + returning perm_id into v_expansion_menu_perm_id; + else + update sys_permission + set parent_id = v_business_parent_id, + name = '拓展', + perm_type = 'menu', + level = 2, + path = null, + component = null, + icon = coalesce(nullif(icon, ''), 'TeamOutlined'), + sort_order = coalesce(sort_order, 20), + is_visible = 0, + status = 1, + description = 'CRM前台拓展管理页面权限分组,仅用于权限管理,不在后台侧边栏显示', + meta = coalesce(meta, '{}'::jsonb), + is_deleted = 0, + updated_at = now() + where perm_id = v_expansion_menu_perm_id; + end if; + + select perm_id + into v_expansion_create_perm_id + from sys_permission + where code = 'expansion:create' + order by perm_id + limit 1; + + if v_expansion_create_perm_id is null then + insert into sys_permission ( + parent_id, name, code, perm_type, level, path, component, icon, + sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at + ) values ( + v_expansion_menu_perm_id, '新增拓展', 'expansion:create', 'button', 3, + null, null, null, 1, 1, 1, '控制CRM前台拓展管理页面的新增按钮和销售/渠道拓展创建接口', '{}'::jsonb, 0, now(), now() + ) + returning perm_id into v_expansion_create_perm_id; + else + update sys_permission + set parent_id = v_expansion_menu_perm_id, + name = '新增拓展', + perm_type = 'button', + level = 3, + path = null, + component = null, + icon = null, + sort_order = 1, + is_visible = 1, + status = 1, + description = '控制CRM前台拓展管理页面的新增按钮和销售/渠道拓展创建接口', + meta = '{}'::jsonb, + is_deleted = 0, + updated_at = now() + where perm_id = v_expansion_create_perm_id; + end if; + + if v_has_role_permission_tenant then + insert into sys_role_permission (role_id, perm_id, tenant_id, is_deleted, created_at, updated_at) + select distinct r.role_id, permission_source.perm_id, r.tenant_id, 0, now(), now() + from sys_role r + cross join ( + select unnest(array[v_business_parent_id, v_expansion_menu_perm_id, v_expansion_create_perm_id]) as perm_id + ) permission_source + where coalesce(r.is_deleted, 0) = 0 + and permission_source.perm_id is not null + and ( + r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN') + or r.role_name ilike '%管理员%' + or r.role_name ilike '%admin%' + ) + and not exists ( + select 1 + from sys_role_permission rp + where rp.role_id = r.role_id + and rp.perm_id = permission_source.perm_id + ); + + update sys_role_permission rp + set tenant_id = coalesce(rp.tenant_id, r.tenant_id), + is_deleted = 0, + updated_at = now() + from sys_role r + where rp.role_id = r.role_id + and rp.perm_id in (v_business_parent_id, v_expansion_menu_perm_id, v_expansion_create_perm_id) + and coalesce(r.is_deleted, 0) = 0 + and ( + r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN') + or r.role_name ilike '%管理员%' + or r.role_name ilike '%admin%' + ); + else + insert into sys_role_permission (role_id, perm_id, is_deleted, created_at, updated_at) + select distinct r.role_id, permission_source.perm_id, 0, now(), now() + from sys_role r + cross join ( + select unnest(array[v_business_parent_id, v_expansion_menu_perm_id, v_expansion_create_perm_id]) as perm_id + ) permission_source + where coalesce(r.is_deleted, 0) = 0 + and permission_source.perm_id is not null + and ( + r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN') + or r.role_name ilike '%管理员%' + or r.role_name ilike '%admin%' + ) + and not exists ( + select 1 + from sys_role_permission rp + where rp.role_id = r.role_id + and rp.perm_id = permission_source.perm_id + ); + + update sys_role_permission rp + set is_deleted = 0, + updated_at = now() + from sys_role r + where rp.role_id = r.role_id + and rp.perm_id in (v_business_parent_id, v_expansion_menu_perm_id, v_expansion_create_perm_id) + and coalesce(r.is_deleted, 0) = 0 + and ( + r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN') + or r.role_name ilike '%管理员%' + or r.role_name ilike '%admin%' + ); + end if; +end $$; + +commit; diff --git a/sql/20260630_opportunity_create_permission_pg17.sql b/sql/20260630_opportunity_create_permission_pg17.sql new file mode 100644 index 00000000..0cdbf0e8 --- /dev/null +++ b/sql/20260630_opportunity_create_permission_pg17.sql @@ -0,0 +1,278 @@ +-- 20260630 +-- 新增前台业务按钮权限点: +-- opportunity:create 新增商机 +-- expansion:create 新增拓展 +-- 执行后可在 frontend1 权限管理与角色权限绑定中维护这些权限。 +-- 可重复执行;已执行过旧版本的环境,重新执行本脚本会修复父子关系和管理员授权。 + +begin; + +set search_path to public; + +do $$ +declare + v_business_parent_id bigint; + v_expansion_menu_perm_id bigint; + v_expansion_create_perm_id bigint; + v_menu_perm_id bigint; + v_create_perm_id bigint; + v_has_role_permission_tenant boolean; +begin + perform setval('sys_permission_perm_id_seq', coalesce((select max(perm_id) from sys_permission), 0) + 1, false); + perform setval('sys_role_permission_id_seq', coalesce((select max(id) from sys_role_permission), 0) + 1, false); + + select exists ( + select 1 + from information_schema.columns + where table_schema = current_schema() + and table_name = 'sys_role_permission' + and column_name = 'tenant_id' + ) into v_has_role_permission_tenant; + + select perm_id + into v_business_parent_id + from sys_permission + where code = 'crm' + order by perm_id + limit 1; + + if v_business_parent_id is null then + insert into sys_permission ( + parent_id, name, code, perm_type, level, path, component, icon, + sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at + ) values ( + null, 'CRM业务', 'crm', 'directory', 1, null, null, 'AppstoreOutlined', + 10, 0, 1, 'CRM前台业务权限分组,仅用于权限管理,不在后台侧边栏显示', '{}'::jsonb, 0, now(), now() + ) + returning perm_id into v_business_parent_id; + else + update sys_permission + set name = 'CRM业务', + perm_type = 'directory', + level = 1, + path = null, + component = null, + icon = coalesce(nullif(icon, ''), 'AppstoreOutlined'), + sort_order = coalesce(sort_order, 10), + is_visible = 0, + status = 1, + description = 'CRM前台业务权限分组,仅用于权限管理,不在后台侧边栏显示', + meta = coalesce(meta, '{}'::jsonb), + is_deleted = 0, + updated_at = now() + where perm_id = v_business_parent_id; + end if; + + select perm_id + into v_expansion_menu_perm_id + from sys_permission + where code = 'menu:expansion' + order by perm_id + limit 1; + + if v_expansion_menu_perm_id is null then + insert into sys_permission ( + parent_id, name, code, perm_type, level, path, component, icon, + sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at + ) values ( + v_business_parent_id, '拓展', 'menu:expansion', 'menu', 2, + null, null, 'TeamOutlined', 20, 0, 1, + 'CRM前台拓展管理页面权限分组,仅用于权限管理,不在后台侧边栏显示', '{}'::jsonb, 0, now(), now() + ) + returning perm_id into v_expansion_menu_perm_id; + else + update sys_permission + set parent_id = v_business_parent_id, + name = '拓展', + perm_type = 'menu', + level = 2, + path = null, + component = null, + icon = coalesce(nullif(icon, ''), 'TeamOutlined'), + sort_order = coalesce(sort_order, 20), + is_visible = 0, + status = 1, + description = 'CRM前台拓展管理页面权限分组,仅用于权限管理,不在后台侧边栏显示', + meta = coalesce(meta, '{}'::jsonb), + is_deleted = 0, + updated_at = now() + where perm_id = v_expansion_menu_perm_id; + end if; + + select perm_id + into v_expansion_create_perm_id + from sys_permission + where code = 'expansion:create' + order by perm_id + limit 1; + + if v_expansion_create_perm_id is null then + insert into sys_permission ( + parent_id, name, code, perm_type, level, path, component, icon, + sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at + ) values ( + v_expansion_menu_perm_id, '新增拓展', 'expansion:create', 'button', 3, + null, null, null, 1, 1, 1, '控制CRM前台拓展管理页面的新增按钮和销售/渠道拓展创建接口', '{}'::jsonb, 0, now(), now() + ) + returning perm_id into v_expansion_create_perm_id; + else + update sys_permission + set parent_id = v_expansion_menu_perm_id, + name = '新增拓展', + perm_type = 'button', + level = 3, + path = null, + component = null, + icon = null, + sort_order = 1, + is_visible = 1, + status = 1, + description = '控制CRM前台拓展管理页面的新增按钮和销售/渠道拓展创建接口', + meta = '{}'::jsonb, + is_deleted = 0, + updated_at = now() + where perm_id = v_expansion_create_perm_id; + end if; + + select perm_id + into v_menu_perm_id + from sys_permission + where code = 'menu:opportunities' + order by perm_id + limit 1; + + if v_menu_perm_id is null then + insert into sys_permission ( + parent_id, name, code, perm_type, level, path, component, icon, + sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at + ) values ( + v_business_parent_id, '商机', 'menu:opportunities', 'menu', 2, + null, null, 'BriefcaseOutlined', 30, 0, 1, + 'CRM前台商机储备页面权限分组,仅用于权限管理,不在后台侧边栏显示', '{}'::jsonb, 0, now(), now() + ) + returning perm_id into v_menu_perm_id; + else + update sys_permission + set parent_id = v_business_parent_id, + name = '商机', + perm_type = 'menu', + level = 2, + path = null, + component = null, + icon = coalesce(nullif(icon, ''), 'BriefcaseOutlined'), + sort_order = coalesce(sort_order, 30), + is_visible = 0, + status = 1, + description = 'CRM前台商机储备页面权限分组,仅用于权限管理,不在后台侧边栏显示', + meta = coalesce(meta, '{}'::jsonb), + is_deleted = 0, + updated_at = now() + where perm_id = v_menu_perm_id; + end if; + + select perm_id + into v_create_perm_id + from sys_permission + where code = 'opportunity:create' + order by perm_id + limit 1; + + if v_create_perm_id is null then + insert into sys_permission ( + parent_id, name, code, perm_type, level, path, component, icon, + sort_order, is_visible, status, description, meta, is_deleted, created_at, updated_at + ) values ( + v_menu_perm_id, '新增商机', 'opportunity:create', 'button', 3, + null, null, null, 1, 1, 1, '控制CRM前台商机储备页面的新增商机按钮和创建接口', '{}'::jsonb, 0, now(), now() + ) + returning perm_id into v_create_perm_id; + else + update sys_permission + set parent_id = v_menu_perm_id, + name = '新增商机', + perm_type = 'button', + level = 3, + path = null, + component = null, + icon = null, + sort_order = 1, + is_visible = 1, + status = 1, + description = '控制CRM前台商机储备页面的新增商机按钮和创建接口', + meta = '{}'::jsonb, + is_deleted = 0, + updated_at = now() + where perm_id = v_create_perm_id; + end if; + + if v_has_role_permission_tenant then + insert into sys_role_permission (role_id, perm_id, tenant_id, is_deleted, created_at, updated_at) + select distinct r.role_id, permission_source.perm_id, r.tenant_id, 0, now(), now() + from sys_role r + cross join ( + select unnest(array[v_business_parent_id, v_expansion_menu_perm_id, v_expansion_create_perm_id, v_menu_perm_id, v_create_perm_id]) as perm_id + ) permission_source + where coalesce(r.is_deleted, 0) = 0 + and permission_source.perm_id is not null + and ( + r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN') + or r.role_name ilike '%管理员%' + or r.role_name ilike '%admin%' + ) + and not exists ( + select 1 + from sys_role_permission rp + where rp.role_id = r.role_id + and rp.perm_id = permission_source.perm_id + ); + + update sys_role_permission rp + set tenant_id = coalesce(rp.tenant_id, r.tenant_id), + is_deleted = 0, + updated_at = now() + from sys_role r + where rp.role_id = r.role_id + and rp.perm_id in (v_business_parent_id, v_expansion_menu_perm_id, v_expansion_create_perm_id, v_menu_perm_id, v_create_perm_id) + and coalesce(r.is_deleted, 0) = 0 + and ( + r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN') + or r.role_name ilike '%管理员%' + or r.role_name ilike '%admin%' + ); + else + insert into sys_role_permission (role_id, perm_id, is_deleted, created_at, updated_at) + select distinct r.role_id, permission_source.perm_id, 0, now(), now() + from sys_role r + cross join ( + select unnest(array[v_business_parent_id, v_expansion_menu_perm_id, v_expansion_create_perm_id, v_menu_perm_id, v_create_perm_id]) as perm_id + ) permission_source + where coalesce(r.is_deleted, 0) = 0 + and permission_source.perm_id is not null + and ( + r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN') + or r.role_name ilike '%管理员%' + or r.role_name ilike '%admin%' + ) + and not exists ( + select 1 + from sys_role_permission rp + where rp.role_id = r.role_id + and rp.perm_id = permission_source.perm_id + ); + + update sys_role_permission rp + set is_deleted = 0, + updated_at = now() + from sys_role r + where rp.role_id = r.role_id + and rp.perm_id in (v_business_parent_id, v_expansion_menu_perm_id, v_expansion_create_perm_id, v_menu_perm_id, v_create_perm_id) + and coalesce(r.is_deleted, 0) = 0 + and ( + r.role_code in ('TENANT_ADMIN', 'ADMIN', 'SYS_ADMIN', 'PLATFORM_ADMIN', 'SUPER_ADMIN') + or r.role_name ilike '%管理员%' + or r.role_name ilike '%admin%' + ); + end if; +end $$; + +commit;