diff --git a/src/main/java/com/gxwebsoft/common/system/controller/TenantController.java b/src/main/java/com/gxwebsoft/common/system/controller/TenantController.java index 957ffac..0c5fb04 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/TenantController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/TenantController.java @@ -17,6 +17,7 @@ import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.annotation.OperationLog; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; @@ -24,6 +25,7 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -49,6 +51,9 @@ public class TenantController extends BaseController { private RedisUtil redisUtil; @Resource private UserService userService; + // cms权限参考租户, 默认取平台租户 + @Value("${websoft.cms.ref-tenant-id:10257}") + private Integer cmsRefTenantId; @Operation(summary = "分页查询租户") @GetMapping("/page") @@ -215,6 +220,40 @@ public class TenantController extends BaseController { return success(menus); } + @OperationLog + @Operation(summary = "授予当前租户cms建站权限") + @PostMapping("/grantCmsPermission") + public ApiResult grantCmsPermission(@RequestBody(required = false) Map body) { + final Integer curTenant = getTenantId(); + if (curTenant == null) { + return fail("获取当前租户失败, 请重新登录"); + } + // 参考租户优先级: 请求参数 > 企业当前使用的租户模板 > 配置项websoft.cms.ref-tenant-id + Integer refTenant = null; + final Object refTenantId = body == null ? null : body.get("refTenantId"); + if (refTenantId instanceof Number) { + refTenant = ((Number) refTenantId).intValue(); + } else if (refTenantId != null && refTenantId.toString().matches("\\d+")) { + refTenant = Integer.valueOf(refTenantId.toString()); + } + if (refTenant == null) { + final Company company = getCompany(); + if (company != null && company.getPlanId() != null) { + refTenant = company.getPlanId(); + } + } + if (refTenant == null) { + refTenant = cmsRefTenantId; + } + if (refTenant == null) { + return fail("未找到参考租户, 请配置websoft.cms.ref-tenant-id"); + } + if (refTenant.equals(curTenant)) { + return fail("参考租户不能与当前租户相同"); + } + return tenantService.grantCmsPermission(refTenant, curTenant); + } + @Operation(summary = "创建租户") @PostMapping("/saveByPhone") public ApiResult saveByPhone(@RequestBody Tenant tenant) { diff --git a/src/main/java/com/gxwebsoft/common/system/service/TenantService.java b/src/main/java/com/gxwebsoft/common/system/service/TenantService.java index ec46196..045cfe7 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/TenantService.java +++ b/src/main/java/com/gxwebsoft/common/system/service/TenantService.java @@ -1,6 +1,7 @@ package com.gxwebsoft.common.system.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.system.entity.Company; import com.gxwebsoft.common.system.entity.Tenant; @@ -45,4 +46,14 @@ public interface TenantService extends IService { boolean destructionAll(Integer tenantId); Tenant getByCodeRel(String code); + + /** + * 授予当前租户cms建站权限 + * 从参考租户复制cms:*菜单到当前租户, 并绑定到当前租户的超级管理员角色 + * + * @param refTenant 参考租户id(已配置好cms菜单的租户) + * @param curTenant 当前租户id + * @return ApiResult + */ + ApiResult grantCmsPermission(Integer refTenant, Integer curTenant); } diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/TenantServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantServiceImpl.java index aafb255..655afc2 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/TenantServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantServiceImpl.java @@ -2,21 +2,34 @@ package com.gxwebsoft.common.system.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.common.core.Constants; import com.gxwebsoft.common.core.utils.CommonUtil; import com.gxwebsoft.common.core.utils.DomainUtil; import com.gxwebsoft.common.core.utils.RedisUtil; import com.gxwebsoft.common.system.entity.*; +import com.gxwebsoft.common.system.mapper.MenuMapper; import com.gxwebsoft.common.system.mapper.TenantMapper; import com.gxwebsoft.common.system.param.MenuParam; import com.gxwebsoft.common.system.service.*; import com.gxwebsoft.common.system.param.TenantParam; +import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * 租户Service实现 @@ -26,12 +39,18 @@ import java.util.List; */ @Service public class TenantServiceImpl extends ServiceImpl implements TenantService { + // cms建站权限标识前缀 + private static final String CMS_AUTHORITY_PREFIX = "cms:"; @Resource private CompanyService companyService; @Resource private MenuService menuService; @Resource + private MenuMapper menuMapper; + @Resource + private RoleMenuService roleMenuService; + @Resource private RoleService roleService; @Resource private UserRoleService userRoleService; @@ -632,4 +651,138 @@ public class TenantServiceImpl extends ServiceImpl impleme return param.getOne(baseMapper.selectListRel(param)); } + @Override + @Transactional(rollbackFor = {Exception.class}) + public ApiResult grantCmsPermission(Integer refTenant, Integer curTenant) { + // 1.当前租户的超级管理员角色, 必须带租户条件 + final Role superAdmin = roleService.getOne(new LambdaQueryWrapper() + .eq(Role::getRoleCode, "superAdmin") + .eq(Role::getTenantId, curTenant) + .last("limit 1")); + if (superAdmin == null) { + return new ApiResult<>(Constants.RESULT_ERROR_CODE, "当前租户没有superAdmin角色, 无法授权"); + } + final Integer roleId = superAdmin.getRoleId(); + + // 2.跨租户读取参考租户的全部未删除菜单 + final MenuParam refParam = new MenuParam(); + refParam.setTenantId(refTenant); + refParam.setDeleted(0); + final List refMenus = menuMapper.getMenuByClone(refParam); + if (CollectionUtils.isEmpty(refMenus)) { + return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的菜单"); + } + final Map refMenuMap = new HashMap<>(); + for (Menu menu : refMenus) { + refMenuMap.put(menu.getMenuId(), menu); + } + + // 3.筛选cms:*子树, 权限标识以cms:开头的节点 + 其全部祖先, 保证父级完整 + final Set cmsMenuIds = new HashSet<>(); + for (Menu menu : refMenus) { + if (menu.getAuthority() == null || !menu.getAuthority().startsWith(CMS_AUTHORITY_PREFIX)) { + continue; + } + Menu current = menu; + // add返回false说明该节点及其祖先已收录, 无需继续向上追溯 + while (current != null && cmsMenuIds.add(current.getMenuId())) { + final Integer parentId = current.getParentId(); + current = (parentId == null || parentId == 0) ? null : refMenuMap.get(parentId); + } + } + if (cmsMenuIds.isEmpty()) { + return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有cms权限菜单"); + } + + // 4.当前租户已有菜单, 用于去重, 保证接口幂等 + final MenuParam curParam = new MenuParam(); + curParam.setTenantId(curTenant); + curParam.setDeleted(0); + final Map curMenuKeys = new HashMap<>(); + for (Menu menu : menuMapper.getMenuByClone(curParam)) { + curMenuKeys.putIfAbsent(getMenuKey(menu), menu.getMenuId()); + } + + // 5.按层级复制菜单, 父菜单先于子菜单插入 + final List sources = refMenus.stream() + .filter(d -> cmsMenuIds.contains(d.getMenuId())) + .sorted(Comparator.comparingInt((Menu d) -> getMenuDepth(d, refMenuMap))) + .collect(Collectors.toList()); + final Map menuIdMapping = new HashMap<>(); + int menuCount = 0; + for (Menu source : sources) { + // 已存在的菜单不重复插入, 直接复用其菜单id作为子菜单的父级 + final Integer existsMenuId = curMenuKeys.get(getMenuKey(source)); + if (existsMenuId != null) { + menuIdMapping.put(source.getMenuId(), existsMenuId); + continue; + } + final Menu menu = new Menu(); + menu.setParentId(source.getParentId() == null ? 0 : menuIdMapping.getOrDefault(source.getParentId(), 0)); + menu.setTitle(source.getTitle()); + menu.setPath(source.getPath()); + menu.setComponent(source.getComponent()); + menu.setModules(source.getModules()); + menu.setModulesUrl(source.getModulesUrl()); + menu.setMenuType(source.getMenuType()); + menu.setSortNumber(source.getSortNumber()); + menu.setAuthority(source.getAuthority()); + menu.setIcon(source.getIcon()); + menu.setHide(source.getHide()); + menu.setMeta(source.getMeta()); + menu.setTenantId(curTenant); + menuService.save(menu); + menuIdMapping.put(source.getMenuId(), menu.getMenuId()); + curMenuKeys.putIfAbsent(getMenuKey(menu), menu.getMenuId()); + menuCount++; + } + + // 6.绑定cms菜单到超级管理员角色, 已绑定的跳过 + final Set boundMenuIds = roleMenuService.list(new LambdaQueryWrapper() + .eq(RoleMenu::getRoleId, roleId)) + .stream().map(RoleMenu::getMenuId).collect(Collectors.toSet()); + final List roleMenus = new ArrayList<>(); + for (Integer menuId : menuIdMapping.values()) { + if (menuId == null || boundMenuIds.contains(menuId)) { + continue; + } + final RoleMenu roleMenu = new RoleMenu(); + roleMenu.setRoleId(roleId); + roleMenu.setMenuId(menuId); + roleMenu.setTenantId(curTenant); + roleMenus.add(roleMenu); + } + if (!roleMenus.isEmpty()) { + roleMenuService.saveBatch(roleMenus); + } + + final Map data = new HashMap<>(); + data.put("refTenantId", refTenant); + data.put("tenantId", curTenant); + data.put("roleId", roleId); + data.put("menuTotal", menuIdMapping.size()); + data.put("menuCreated", menuCount); + data.put("roleMenuCreated", roleMenus.size()); + return new ApiResult<>(Constants.RESULT_OK_CODE, "授权成功", data); + } + + // 菜单去重标识, 优先使用权限标识, 目录类菜单权限标识为空时按标题+路由匹配 + private String getMenuKey(Menu menu) { + if (StrUtil.isNotBlank(menu.getAuthority())) { + return "authority:" + menu.getAuthority(); + } + return "menu:" + StrUtil.nullToEmpty(menu.getTitle()) + "@" + StrUtil.nullToEmpty(menu.getPath()); + } + + // 菜单层级, 用于保证父菜单先于子菜单插入 + private int getMenuDepth(Menu menu, Map menuMap) { + int depth = 0; + Menu current = menu; + while (current != null && current.getParentId() != null && current.getParentId() != 0 && depth < 20) { + current = menuMap.get(current.getParentId()); + depth++; + } + return depth; + } + }