feat(tenant): 授予当前租户cms建站权限
- 新增接口支持根据参考租户复制cms权限菜单到当前租户 - 支持请求参数指定参考租户ID,默认为配置项或企业模板 - 复制cms:*权限标识菜单及其祖先,保证菜单层级完整 - 实现菜单去重与接口幂等,避免重复插入菜单 - 将复制菜单绑定到当前租户超级管理员角色 - 添加相关服务层逻辑及事务保障 - 新增配置项websoft.cms.ref-tenant-id,指定默认参考租户ID
This commit is contained in:
@@ -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<String, Object> 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) {
|
||||
|
||||
@@ -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<Tenant> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<TenantMapper, Tenant> 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<TenantMapper, Tenant> 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<Role>()
|
||||
.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<Menu> refMenus = menuMapper.getMenuByClone(refParam);
|
||||
if (CollectionUtils.isEmpty(refMenus)) {
|
||||
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的菜单");
|
||||
}
|
||||
final Map<Integer, Menu> refMenuMap = new HashMap<>();
|
||||
for (Menu menu : refMenus) {
|
||||
refMenuMap.put(menu.getMenuId(), menu);
|
||||
}
|
||||
|
||||
// 3.筛选cms:*子树, 权限标识以cms:开头的节点 + 其全部祖先, 保证父级完整
|
||||
final Set<Integer> 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<String, Integer> curMenuKeys = new HashMap<>();
|
||||
for (Menu menu : menuMapper.getMenuByClone(curParam)) {
|
||||
curMenuKeys.putIfAbsent(getMenuKey(menu), menu.getMenuId());
|
||||
}
|
||||
|
||||
// 5.按层级复制菜单, 父菜单先于子菜单插入
|
||||
final List<Menu> sources = refMenus.stream()
|
||||
.filter(d -> cmsMenuIds.contains(d.getMenuId()))
|
||||
.sorted(Comparator.comparingInt((Menu d) -> getMenuDepth(d, refMenuMap)))
|
||||
.collect(Collectors.toList());
|
||||
final Map<Integer, Integer> 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<Integer> boundMenuIds = roleMenuService.list(new LambdaQueryWrapper<RoleMenu>()
|
||||
.eq(RoleMenu::getRoleId, roleId))
|
||||
.stream().map(RoleMenu::getMenuId).collect(Collectors.toSet());
|
||||
final List<RoleMenu> 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<String, Object> 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<Integer, Menu> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user