新增权限点增加

main
kangwenjing 2026-07-01 09:11:17 +08:00
parent 8a7ee5f066
commit c6b695169e
12 changed files with 690 additions and 18 deletions

View File

@ -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);
}
}
}

View File

@ -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,

View File

@ -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();

View File

@ -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);

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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;

View File

@ -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;