✨ 组织树独立权限、平台公司回写、合同计费客商角色与查看菜单补齐

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 03:15:42 +08:00
parent a0e30d5834
commit 69f9c9af7f
26 changed files with 745 additions and 98 deletions
@@ -126,8 +126,11 @@ public class DeptController extends BladeController {
/**
* 获取部门树形结构
* <p>
* 登录即可调用;数据范围由角色「数据权限」中 {@code dept_tree}(树形结构)单独配置,
* 未配置时返回空树(默认不展示)。
*/
@PreAuth(menu = "dept")
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/tree")
@ApiOperationSupport(order = 4)
@Operation(summary = "树形结构", description = "树形结构")
@@ -139,8 +142,9 @@ public class DeptController extends BladeController {
* 懒加载获取部门树形结构
* <p>
* tenantId 由 Service 层完整决策:超管入参为空表示查全部,非超管强制覆盖为会话租户。
* 数据范围与 {@link #tree} 相同,由 {@code dept_tree} 单独配置,未配置时返回空树。
*/
@PreAuth(menu = "dept")
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/lazy-tree")
@ApiOperationSupport(order = 5)
@Operation(summary = "懒加载树形结构", description = "树形结构")
@@ -66,6 +66,16 @@ public interface IRoleMenuDataScopeService extends BaseService<RoleMenuDataScope
*/
RoleDataScopeVO resolveByMenuCode(List<Long> roleIds, String menuCode, List<Long> userDeptIds);
/**
* 按菜单编码解析角色数据权限;菜单不存在或未配置时返回空权限(不回落为全部组织)
*
* @param roleIds 角色ID集合
* @param menuCode 菜单编码
* @param userDeptIds 用户所属组织
* @return 解析结果
*/
RoleDataScopeVO resolveByMenuCodeOrEmpty(List<Long> roleIds, String menuCode, List<Long> userDeptIds);
/**
* 合并用户全部角色菜单配置为最宽数据权限
*
@@ -224,13 +224,18 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
.toList();
}
/**
* 组织树专用数据权限菜单编码(角色「数据权限」中单独配置;未配置时组织树默认不展示)。
*/
public static final String MENU_DEPT_TREE = "dept_tree";
/**
* 解析当前登录用户的组织树数据权限。
* <p>
* 超管或角色菜单数据权限最宽合并为「全部组织」时不过滤;
* 其余按角色配置的组织并集过滤。仅「本人创建」时按用户所属组织及下级展示组织树。
* 仅读取 {@link #MENU_DEPT_TREE}「树形结构」菜单的数据权限配置,不再合并其他业务菜单;
* 超管不过滤;未配置该菜单数据权限时返回空树(默认不显示)。
*
* @return 过滤上下文;无法识别用户时按空权限处理
* @return 过滤上下文;无法识别用户或未配置时按空权限处理
*/
private DeptDataScopeFilter resolveDeptDataScopeFilter() {
if (AuthUtil.isAdministrator()) {
@@ -246,8 +251,11 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (Func.isNotEmpty(user) && Func.isNotEmpty(user.getDeptId())) {
userDeptIds = Func.toLongList(user.getDeptId());
}
RoleDataScopeVO scope = roleMenuDataScopeService.resolveWidest(roleIds, userDeptIds);
if (scope == null || scope.isAllDept()) {
RoleDataScopeVO scope = roleMenuDataScopeService.resolveByMenuCodeOrEmpty(roleIds, MENU_DEPT_TREE, userDeptIds);
if (scope == null) {
return DeptDataScopeFilter.empty();
}
if (scope.isAllDept()) {
return DeptDataScopeFilter.all();
}
LinkedHashSet<Long> allowedDeptIds = new LinkedHashSet<>();
@@ -665,12 +673,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
dept.setAncestors(parent.getAncestors() + StringPool.COMMA + dept.getParentId());
}
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
if (dept.getIsPlatformCompany() == null) {
dept.setIsPlatformCompany(0);
}
if (dept.getIsBusinessDept() == null) {
dept.setIsBusinessDept(0);
}
// 统一规范为 0/1,避免前端传空串、布尔或其它类型导致更新未生效
dept.setIsPlatformCompany(normalizeYesNoFlag(dept.getIsPlatformCompany()));
dept.setIsBusinessDept(normalizeYesNoFlag(dept.getIsBusinessDept()));
// 排序未填时默认 1
if (dept.getSort() == null) {
dept.setSort(1);
@@ -679,9 +684,24 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
throw new ServiceException("租户ID不能为空");
}
validateDeptCategory(dept);
validateDeptCode(dept, parent);
validateDeptCode(dept);
validateContractInitialSerial(dept);
return saveOrUpdate(dept);
boolean saved = saveOrUpdate(dept);
if (saved && Func.isNotEmpty(dept.getId())) {
// 显式回写两个业务标志,避免部分更新路径漏改
this.update(Wrappers.<Dept>lambdaUpdate()
.set(Dept::getIsPlatformCompany, dept.getIsPlatformCompany())
.set(Dept::getIsBusinessDept, dept.getIsBusinessDept())
.eq(Dept::getId, dept.getId()));
}
return saved;
}
/**
* 将是否类标志规范为 0/1;空值按 0 处理。
*/
private Integer normalizeYesNoFlag(Integer flag) {
return Integer.valueOf(1).equals(flag) ? 1 : 0;
}
@Override
@@ -881,9 +901,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
}
}
private void validateDeptCode(Dept dept, Dept parent) {
private void validateDeptCode(Dept dept) {
String deptCode = dept.getDeptCode();
// 组织编码非必填,为空时存 null,避免唯一索引冲突
// 组织编码非必填,为空时存 null,避免唯一索引冲突;不再校验 GWWL/上级编码+三位数字格式
if (StringUtil.isBlank(deptCode)) {
dept.setDeptCode(null);
return;
@@ -892,26 +912,6 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (deptCode.length() > 30) {
throw new ServiceException("组织编码不能超过30个字符");
}
// 顶级组织不依赖上级编码
if (parent == null) {
this.ensureDeptCodeUnique(dept, deptCode);
dept.setDeptCode(deptCode);
return;
}
if (StringUtil.isBlank(parent.getDeptCode())) {
throw new ServiceException("请选择已配置组织编码的上级组织");
}
// 公司:GWWL+三位数字;部门:所属公司编码+三位数字;其他:上级编码+三位数字
String expectedParentCode = parent.getDeptCode();
if (DeptCategory.COMPANY.getCode().equals(dept.getDeptCategory())) {
expectedParentCode = OAConvertConstant.OA_SYNC_ROOT_DEPT_CODE;
} else if (DeptCategory.DEPT.getCode().equals(dept.getDeptCategory())
&& StringUtil.isNotBlank(dept.getBelongCompanyCode())) {
expectedParentCode = dept.getBelongCompanyCode();
}
if (!org.springblade.system.util.DeptCodeUtil.matchesChildCode(expectedParentCode, deptCode)) {
throw new ServiceException("组织编码格式应为:" + expectedParentCode + "三位数字(不足补零,中间不加-)");
}
this.ensureDeptCodeUnique(dept, deptCode);
dept.setDeptCode(deptCode);
}
@@ -184,20 +184,44 @@ public class RoleMenuDataScopeServiceImpl extends BaseServiceImpl<RoleMenuDataSc
if (Func.isEmpty(roleIds) || Func.isBlank(menuCode)) {
return RoleDataScopeVO.all();
}
Menu menu = menuMapper.selectOne(Wrappers.<Menu>lambdaQuery()
.eq(Menu::getCode, menuCode)
.eq(Menu::getIsDeleted, 0)
.last("limit 1"));
Menu menu = findMenuByCode(menuCode);
if (Func.isEmpty(menu)) {
return RoleDataScopeVO.all();
}
List<RoleMenuDataScope> scopeList = list(Wrappers.<RoleMenuDataScope>lambdaQuery()
.in(RoleMenuDataScope::getRoleId, roleIds)
.eq(RoleMenuDataScope::getMenuId, menu.getId())
.eq(RoleMenuDataScope::getIsDeleted, 0));
List<RoleMenuDataScope> scopeList = listScopes(roleIds, menu.getId());
return mergeScopes(scopeList, userDeptIds);
}
@Override
public RoleDataScopeVO resolveByMenuCodeOrEmpty(List<Long> roleIds, String menuCode, List<Long> userDeptIds) {
if (Func.isEmpty(roleIds) || Func.isBlank(menuCode)) {
return RoleDataScopeVO.none();
}
Menu menu = findMenuByCode(menuCode);
if (Func.isEmpty(menu)) {
return RoleDataScopeVO.none();
}
List<RoleMenuDataScope> scopeList = listScopes(roleIds, menu.getId());
if (Func.isEmpty(scopeList)) {
return RoleDataScopeVO.none();
}
return mergeScopes(scopeList, userDeptIds);
}
private Menu findMenuByCode(String menuCode) {
return menuMapper.selectOne(Wrappers.<Menu>lambdaQuery()
.eq(Menu::getCode, menuCode)
.eq(Menu::getIsDeleted, 0)
.last("limit 1"));
}
private List<RoleMenuDataScope> listScopes(List<Long> roleIds, Long menuId) {
return list(Wrappers.<RoleMenuDataScope>lambdaQuery()
.in(RoleMenuDataScope::getRoleId, roleIds)
.eq(RoleMenuDataScope::getMenuId, menuId)
.eq(RoleMenuDataScope::getIsDeleted, 0));
}
@Override
public RoleDataScopeVO resolveWidest(List<Long> roleIds, List<Long> userDeptIds) {
if (Func.isEmpty(roleIds)) {