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<string, { expiresAt: number; value: unknown }>();
 const inFlightRequestCache = new Map<string, Promise<unknown>>();
 let authExpiryTimer: number | null = null;
@@ -1235,6 +1246,29 @@ export async function refreshCurrentUser() {
   return profile;
 }
 
+export async function listMyPermissions() {
+  return getCachedAuthedRequest<UserPermission[]>(
+    MY_PERMISSIONS_CACHE_KEY,
+    () => request<UserPermission[]>("/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<DashboardHome>("/api/dashboard/home", undefined, true);
 }
@@ -1650,7 +1684,7 @@ function writeCachedValue<T>(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<ExpansionExportFilters>(() => loadExpansionExportPreferences());
+  const [permissionCodes, setPermissionCodes] = useState<string[] | null>(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<CreateSalesExpansionPayload>(defaultSalesForm);
   const [editChannelForm, setEditChannelForm] = useState<CreateChannelExpansionPayload>(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() {
             <Download className="crm-icon-md" />
             <span className="hidden sm:inline">{exporting ? "导出中..." : "导出"}</span>
           </button>
-          <button
-            onClick={handleOpenCreate}
-            className={cn("crm-btn-sm crm-btn-primary flex items-center gap-2", disableMobileMotion ? "active:scale-100" : "active:scale-95")}
-          >
-            <Plus className="crm-icon-md" />
-            <span className="hidden sm:inline">新增</span>
-          </button>
+          {canCreateExpansion ? (
+            <button
+              onClick={handleOpenCreate}
+              className={cn("crm-btn-sm crm-btn-primary flex items-center gap-2", disableMobileMotion ? "active:scale-100" : "active:scale-95")}
+            >
+              <Plus className="crm-icon-md" />
+              <span className="hidden sm:inline">新增</span>
+            </button>
+          ) : null}
         </div>
       </header>
 
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<OpportunityExportFilters>(() => loadOpportunityExportPreferences());
+  const [permissionCodes, setPermissionCodes] = useState<string[] | null>(null);
   const [error, setError] = useState("");
   const [exportError, setExportError] = useState("");
   const [items, setItems] = useState<OpportunityItem[]>([]);
@@ -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() {
             <Download className="crm-icon-md" />
             <span className="hidden sm:inline">{exporting ? "导出中..." : "导出"}</span>
           </button>
-          <button
-            onClick={handleOpenCreate}
-            className={cn("crm-btn-sm crm-btn-primary flex items-center gap-2", disableMobileMotion ? "active:scale-100" : "active:scale-95")}
-          >
-            <Plus className="crm-icon-md" />
-            <span className="hidden sm:inline">新增商机</span>
-          </button>
+          {canCreateOpportunity ? (
+            <button
+              onClick={handleOpenCreate}
+              className={cn("crm-btn-sm crm-btn-primary flex items-center gap-2", disableMobileMotion ? "active:scale-100" : "active:scale-95")}
+            >
+              <Plus className="crm-icon-md" />
+              <span className="hidden sm:inline">新增商机</span>
+            </button>
+          ) : null}
         </div>
       </header>
 
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 @@
     <link rel="icon" type="image/svg+xml" href="/logo.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>UnisBase - 智能会议系统</title>
-    <script type="module" crossorigin src="/assets/index-CibjLtv7.js"></script>
+    <script type="module" crossorigin src="/assets/index-DNoKV8lp.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-CaWPk49l.css">
   </head>
   <body>
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;
