feat(system): 扩展用户部门权限功能
- 在 AdminUserService 中新增 listEnabledUserIdsByDeptIds 方法获取指定部门集合下启用且未离职的用户 ID 集合 - 在 DeptService 中新增 listDescendantDeptIds 方法获得指定部门集合及其所有子孙部门的 ID 集合 - 在 DeptService 中新增 listCodesByIds 方法按 id 集合批量查询部门 code 集合 - 在 OrgLeaderRelationService 中新增 listEffectiveDeptIdsByUserId 方法查询指定用户当前生效的负责人关系所对应的 dept_id 集合 - 在 PermissionApi 中新增 isSuperAdmin 接口判断用户是否超管 - 在 ObjectPermissionApi 中新增 getObjectRolePermissionDetailMerged 接口按 roleId 列表聚合菜单 + 权限码 - 扩展 ProductContextRoleRespVO 添加多角色场景的附加角色名称列表 - 扩展 ProductCreateWithTeamReqVO 支持创建时添加关心人用户 ID 列表 - 优化 ProductMemberServiceImpl 支持同一用户多角色显示,区分主角色和附加角色 - 新增 MEMBER_ACTION_REACTIVATE 复活动作类型用于处理 INACTIVE 成员行重新激活场景 - 在 ObjectStatusModelDO 中新增 progressExcludedFlag 字段控制是否参与上层进度统计 - 更新 AGENTS.md 和 CLAUDE.md 添加 Git 操作纪律规范 - 在 rdms-project-api 中新增多个错误码常量支持角色转移和内置角色配置验证
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package com.njcn.rdms.module.system.api.dept;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.system.service.dept.DeptService;
|
||||
import com.njcn.rdms.module.system.service.dept.OrgLeaderRelationService;
|
||||
import com.njcn.rdms.module.system.service.user.AdminUserService;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 组织负责人 RPC 接口实现
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Hidden
|
||||
public class OrgLeaderApiImpl implements OrgLeaderApi {
|
||||
|
||||
@Resource
|
||||
private OrgLeaderRelationService orgLeaderRelationService;
|
||||
@Resource
|
||||
private DeptService deptService;
|
||||
@Resource
|
||||
private AdminUserService adminUserService;
|
||||
|
||||
@Override
|
||||
public CommonResult<Set<Long>> getReachableUserIds(Long currentUserId) {
|
||||
// 1. 当前用户作为 leader 生效中的 dept_id 集合
|
||||
Set<Long> leaderDeptIds = orgLeaderRelationService.listEffectiveDeptIdsByUserId(currentUserId);
|
||||
if (CollUtil.isEmpty(leaderDeptIds)) {
|
||||
return success(Collections.emptySet());
|
||||
}
|
||||
|
||||
// 2. 含递归子孙节点的 dept_id 集合(按 path 前缀匹配,一次 SQL 完成)
|
||||
Set<Long> allDeptIds = deptService.listDescendantDeptIds(leaderDeptIds);
|
||||
|
||||
// 3. 这些 dept 下启用且未离职的 user_id 集合
|
||||
Set<Long> userIds = adminUserService.listEnabledUserIdsByDeptIds(allDeptIds);
|
||||
|
||||
// 4. 移除自己(自己看自己走通道 1,不在本结果集里)
|
||||
userIds.remove(currentUserId);
|
||||
return success(userIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,8 +17,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
@@ -77,6 +80,62 @@ public class ObjectPermissionApiImpl implements ObjectPermissionApi {
|
||||
return success(detail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<ObjectRolePermissionRespDTO> getObjectRolePermissionDetailMerged(
|
||||
Collection<Long> roleIds, String scopeType, String objectType) {
|
||||
if (roleIds == null || roleIds.isEmpty()) {
|
||||
return success(emptyPermissionDetail());
|
||||
}
|
||||
|
||||
// 拿全部启用的角色,过滤 null 和未启用
|
||||
List<RoleDO> activeRoles = roleIds.stream()
|
||||
.distinct()
|
||||
.map(id -> getEnabledScopedRole(id, scopeType, objectType))
|
||||
.filter(r -> r != null)
|
||||
.toList();
|
||||
if (activeRoles.isEmpty()) {
|
||||
return success(emptyPermissionDetail());
|
||||
}
|
||||
|
||||
// 主角色:按 sort 升序,决胜按 id 升序
|
||||
Comparator<RoleDO> rolePriority = Comparator
|
||||
.comparingInt((RoleDO r) -> r.getSort() == null ? Integer.MAX_VALUE : r.getSort())
|
||||
.thenComparingLong(RoleDO::getId);
|
||||
RoleDO primaryRole = activeRoles.stream().min(rolePriority).orElseThrow();
|
||||
|
||||
// 非主角色名(按 sort 升序保持稳定顺序)
|
||||
List<String> additionalRoleNames = activeRoles.stream()
|
||||
.filter(r -> !r.getId().equals(primaryRole.getId()))
|
||||
.sorted(rolePriority)
|
||||
.map(RoleDO::getName)
|
||||
.toList();
|
||||
|
||||
// 菜单 union(按 menu.id 去重,按 menu.sort 排序)
|
||||
Map<Long, MenuDO> mergedMenus = new LinkedHashMap<>();
|
||||
for (RoleDO role : activeRoles) {
|
||||
for (MenuDO menu : permissionService.getScopedMenusByRoleId(role.getId(), scopeType, objectType)) {
|
||||
mergedMenus.putIfAbsent(menu.getId(), menu);
|
||||
}
|
||||
}
|
||||
List<ObjectMenuRespDTO> menus = mergedMenus.values().stream()
|
||||
.sorted(Comparator.comparingInt(m -> m.getSort() == null ? Integer.MAX_VALUE : m.getSort()))
|
||||
.map(this::convertMenu)
|
||||
.toList();
|
||||
|
||||
// 权限码 union(LinkedHashSet 保持稳定顺序)
|
||||
Set<String> permissions = new LinkedHashSet<>();
|
||||
for (RoleDO role : activeRoles) {
|
||||
permissions.addAll(permissionService.getScopedPermissionsByRoleId(role.getId(), scopeType, objectType));
|
||||
}
|
||||
|
||||
ObjectRolePermissionRespDTO detail = new ObjectRolePermissionRespDTO();
|
||||
detail.setCurrentRole(convertRole(primaryRole));
|
||||
detail.setAdditionalRoleNames(additionalRoleNames);
|
||||
detail.setMenus(menus);
|
||||
detail.setPermissions(permissions);
|
||||
return success(detail);
|
||||
}
|
||||
|
||||
private ObjectRolePermissionRespDTO emptyPermissionDetail() {
|
||||
ObjectRolePermissionRespDTO detail = new ObjectRolePermissionRespDTO();
|
||||
detail.setCurrentRole(null);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.njcn.rdms.module.system.api.permission;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.system.service.permission.PermissionService;
|
||||
import com.njcn.rdms.module.system.service.permission.RoleService;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -22,6 +23,9 @@ public class PermissionApiImpl implements PermissionApi {
|
||||
@Resource
|
||||
private PermissionService permissionService;
|
||||
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
|
||||
@Override
|
||||
public CommonResult<Set<Long>> getUserRoleIdListByRoleIds(Collection<Long> roleIds) {
|
||||
return success(permissionService.getUserRoleIdListByRoleId(roleIds));
|
||||
@@ -37,5 +41,14 @@ public class PermissionApiImpl implements PermissionApi {
|
||||
return success(permissionService.hasAnyRoles(userId, roles));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> isSuperAdmin(Long userId) {
|
||||
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(userId);
|
||||
if (roleIds.isEmpty()) {
|
||||
return success(false);
|
||||
}
|
||||
return success(roleService.hasAnySuperAdmin(roleIds));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.njcn.rdms.module.system.api.permission;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.UserVisibilityConfigRespDTO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.UserVisibilityConfigDO;
|
||||
import com.njcn.rdms.module.system.service.dept.DeptService;
|
||||
import com.njcn.rdms.module.system.service.permission.UserVisibilityConfigService;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 用户可见性配置 RPC 接口实现。
|
||||
*
|
||||
* directionId → directionCode 的转换在 API 端完成,
|
||||
* 业务侧(rdms-project)不需要再 join system_dept。
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Hidden
|
||||
public class UserVisibilityConfigApiImpl implements UserVisibilityConfigApi {
|
||||
|
||||
@Resource
|
||||
private UserVisibilityConfigService userVisibilityConfigService;
|
||||
@Resource
|
||||
private DeptService deptService;
|
||||
|
||||
@Override
|
||||
public CommonResult<UserVisibilityConfigRespDTO> getConfig(Long userId) {
|
||||
UserVisibilityConfigDO cfg = userVisibilityConfigService.getByUserId(userId);
|
||||
if (cfg == null) {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
UserVisibilityConfigRespDTO dto = new UserVisibilityConfigRespDTO();
|
||||
dto.setType(cfg.getVisibilityType());
|
||||
|
||||
if ("directions".equals(cfg.getVisibilityType())
|
||||
&& CollUtil.isNotEmpty(cfg.getVisibleDirectionIds())) {
|
||||
// directionId → directionCode 转换:在 API 端做,避免 rdms-project 再 join system_dept
|
||||
dto.setDirectionCodes(deptService.listCodesByIds(cfg.getVisibleDirectionIds()));
|
||||
}
|
||||
if ("projects".equals(cfg.getVisibilityType())
|
||||
&& CollUtil.isNotEmpty(cfg.getVisibleProjectIds())) {
|
||||
dto.setProjectIds(new HashSet<>(cfg.getVisibleProjectIds()));
|
||||
}
|
||||
return success(dto);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.njcn.rdms.module.system.controller.admin.permission;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.system.controller.admin.permission.vo.userVisibilityConfig.UserVisibilityConfigRespVO;
|
||||
import com.njcn.rdms.module.system.controller.admin.permission.vo.userVisibilityConfig.UserVisibilityConfigSaveReqVO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.UserVisibilityConfigDO;
|
||||
import com.njcn.rdms.module.system.service.permission.UserVisibilityConfigService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 用户数据可见性配置")
|
||||
@RestController
|
||||
@RequestMapping("/system/user-visibility-config")
|
||||
@Validated
|
||||
public class UserVisibilityConfigController {
|
||||
|
||||
@Resource
|
||||
private UserVisibilityConfigService userVisibilityConfigService;
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "查询用户可见性配置")
|
||||
@Parameter(name = "userId", description = "用户ID", required = true, example = "1")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-visibility-config:query')")
|
||||
public CommonResult<UserVisibilityConfigRespVO> getUserVisibilityConfig(@RequestParam("userId") Long userId) {
|
||||
UserVisibilityConfigDO config = userVisibilityConfigService.getByUserId(userId);
|
||||
return success(BeanUtils.toBean(config, UserVisibilityConfigRespVO.class));
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@Operation(summary = "保存用户可见性配置(存在则更新,不存在则新增)")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-visibility-config:save')")
|
||||
public CommonResult<Long> saveUserVisibilityConfig(@Valid @RequestBody UserVisibilityConfigSaveReqVO saveReqVO) {
|
||||
return success(userVisibilityConfigService.saveOrUpdate(saveReqVO));
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除用户可见性配置")
|
||||
@Parameter(name = "userId", description = "用户ID", required = true, example = "1")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-visibility-config:delete')")
|
||||
public CommonResult<Boolean> deleteUserVisibilityConfig(@RequestParam("userId") Long userId) {
|
||||
userVisibilityConfigService.deleteByUserId(userId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.njcn.rdms.module.system.controller.admin.permission.vo.userVisibilityConfig;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 用户可见性配置 Response VO")
|
||||
@Data
|
||||
public class UserVisibilityConfigRespVO {
|
||||
|
||||
@Schema(description = "配置ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户ID", example = "100")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "可见范围类型:all / directions / projects", example = "directions")
|
||||
private String visibilityType;
|
||||
|
||||
@Schema(description = "补充可见方向ID集合", example = "[107, 103]")
|
||||
private List<Long> visibleDirectionIds;
|
||||
|
||||
@Schema(description = "补充可见项目ID集合", example = "[1001, 1002]")
|
||||
private List<Long> visibleProjectIds;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.system.controller.admin.permission.vo.userVisibilityConfig;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 用户可见性配置 Save Request VO")
|
||||
@Data
|
||||
public class UserVisibilityConfigSaveReqVO {
|
||||
|
||||
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "可见范围类型:all / directions / projects",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "directions")
|
||||
@NotBlank(message = "可见范围类型不能为空")
|
||||
private String visibilityType;
|
||||
|
||||
@Schema(description = "补充可见方向ID集合(type=directions 时必填)", example = "[107, 103]")
|
||||
private List<Long> visibleDirectionIds;
|
||||
|
||||
@Schema(description = "补充可见项目ID集合(type=projects 时必填,业务暂不引导)", example = "[1001, 1002]")
|
||||
private List<Long> visibleProjectIds;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.njcn.rdms.module.system.dal.dataobject.permission;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户数据可见性配置 DO
|
||||
*
|
||||
* 每个用户至多一条记录(user_id 有唯一索引),记录该用户在数据可见性维度的配置。
|
||||
*/
|
||||
@TableName(value = "system_user_visibility_config", autoResultMap = true)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserVisibilityConfigDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键 ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*
|
||||
* 唯一约束,每个用户至多一条可见性配置。
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 可见性类型
|
||||
*
|
||||
* 取值:all(全部可见)/ directions(按方向)/ projects(按项目)
|
||||
*/
|
||||
private String visibilityType;
|
||||
|
||||
/**
|
||||
* 可见的方向 ID 列表(JSON 存储)
|
||||
*
|
||||
* visibilityType = "directions" 时有效,存储用户有权查看的方向 ID 集合。
|
||||
* autoResultMap = true 已在 @TableName 上声明,typeHandler 才能正常反序列化。
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<Long> visibleDirectionIds;
|
||||
|
||||
/**
|
||||
* 可见的项目 ID 列表(JSON 存储)
|
||||
*
|
||||
* visibilityType = "projects" 时有效,存储用户有权查看的项目 ID 集合。
|
||||
* 业务暂未消费,预留字段。
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<Long> visibleProjectIds;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.dept.OrgLeaderRelationDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
@@ -23,4 +24,22 @@ public interface OrgLeaderRelationMapper extends BaseMapperX<OrgLeaderRelationDO
|
||||
.orderByDesc(OrgLeaderRelationDO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定用户在指定时间点生效的负责人记录列表
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @param now 当前时间(生效期判断基准)
|
||||
* @return 生效中的负责人关系列表
|
||||
*/
|
||||
default List<OrgLeaderRelationDO> selectEffectiveListByUserId(Long userId, LocalDateTime now) {
|
||||
return selectList(new LambdaQueryWrapperX<OrgLeaderRelationDO>()
|
||||
.eq(OrgLeaderRelationDO::getUserId, userId)
|
||||
// effectiveFrom 为空或 <= now
|
||||
.and(w -> w.isNull(OrgLeaderRelationDO::getEffectiveFrom)
|
||||
.or().le(OrgLeaderRelationDO::getEffectiveFrom, now))
|
||||
// effectiveUntil 为空或 >= now
|
||||
.and(w -> w.isNull(OrgLeaderRelationDO::getEffectiveUntil)
|
||||
.or().ge(OrgLeaderRelationDO::getEffectiveUntil, now)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.njcn.rdms.module.system.dal.mysql.permission;
|
||||
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.UserVisibilityConfigDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户数据可见性配置 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserVisibilityConfigMapper extends BaseMapperX<UserVisibilityConfigDO> {
|
||||
|
||||
/**
|
||||
* 按 user_id 查单条配置(唯一索引保证一人一条)。
|
||||
*/
|
||||
default UserVisibilityConfigDO selectByUserId(Long userId) {
|
||||
return selectOne(new LambdaQueryWrapperX<UserVisibilityConfigDO>()
|
||||
.eq(UserVisibilityConfigDO::getUserId, userId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.njcn.rdms.module.system.framework.permission;
|
||||
|
||||
import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO;
|
||||
import com.njcn.rdms.module.system.enums.permission.ObjectRoleConstants;
|
||||
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
|
||||
import com.njcn.rdms.module.system.service.permission.RoleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 启动时校验 {@link ObjectRoleConstants} 列出的内置对象角色,要求 system_role 表里全部存在、启用、object_type 匹配。
|
||||
* 缺一抛 IllegalStateException 让进程退出 —— 避免运行期按 code 查不到才暴雷。
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ObjectRoleStartupValidator implements ApplicationRunner {
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
for (ObjectRoleConstants required : ObjectRoleConstants.values()) {
|
||||
RoleDO role = roleService.getRoleByCode(
|
||||
required.getCode(),
|
||||
PermissionScopeTypeEnum.OBJECT.getScopeType(),
|
||||
required.getObjectType());
|
||||
if (role == null) {
|
||||
errors.add(String.format("缺失 [%s/%s] (object_type=%s)",
|
||||
required.getCode(), required.getName(), required.getObjectType()));
|
||||
continue;
|
||||
}
|
||||
if (!CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())) {
|
||||
errors.add(String.format("已停用 [%s/%s]", required.getCode(), required.getName()));
|
||||
}
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
String detail = String.join("\n - ", errors);
|
||||
log.error("[ObjectRoleStartupValidator] 内置对象角色校验失败:\n - {}", detail);
|
||||
throw new IllegalStateException("内置对象角色校验失败,请检查 system_role 后重启:\n - " + detail);
|
||||
}
|
||||
log.info("[ObjectRoleStartupValidator] 内置对象角色 {} 条全部就位",
|
||||
ObjectRoleConstants.values().length);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -108,6 +108,15 @@ public interface DeptService {
|
||||
*/
|
||||
Set<Long> getChildDeptIdListFromCache(Long id);
|
||||
|
||||
/**
|
||||
* 获得指定部门集合及其所有子孙部门的 ID 集合。
|
||||
* 基于 system_dept.path 字段前缀匹配,一次 SQL 查询完成,避免递归。
|
||||
*
|
||||
* @param rootDeptIds 根部门 ID 集合
|
||||
* @return 含根节点本身及所有子孙节点的 ID 集合
|
||||
*/
|
||||
Set<Long> listDescendantDeptIds(Collection<Long> rootDeptIds);
|
||||
|
||||
/**
|
||||
* 校验部门们是否有效
|
||||
*
|
||||
@@ -115,4 +124,15 @@ public interface DeptService {
|
||||
*/
|
||||
void validateDeptList(Collection<Long> ids);
|
||||
|
||||
/**
|
||||
* 按 id 集合批量查询部门 code 集合。
|
||||
*
|
||||
* code 为空(null / 空字符串)的记录会被过滤掉。
|
||||
* 用于 API 端将 directionId → directionCode 转换,避免业务侧再 join system_dept。
|
||||
*
|
||||
* @param ids 部门 id 集合
|
||||
* @return 非空 code 的集合
|
||||
*/
|
||||
Set<String> listCodesByIds(Collection<Long> ids);
|
||||
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ import jakarta.annotation.Resource;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertSet;
|
||||
@@ -251,6 +253,36 @@ public class DeptServiceImpl implements DeptService {
|
||||
return convertSet(children, DeptDO::getId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> listDescendantDeptIds(Collection<Long> rootDeptIds) {
|
||||
if (CollUtil.isEmpty(rootDeptIds)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<Long> result = new HashSet<>(rootDeptIds);
|
||||
// 逐个根节点按 path 前缀匹配子孙节点,避免递归查询
|
||||
for (Long rootId : rootDeptIds) {
|
||||
DeptDO root = deptMapper.selectById(rootId);
|
||||
if (root == null || StrUtil.isBlank(root.getPath())) {
|
||||
continue;
|
||||
}
|
||||
List<DeptDO> descendants = deptMapper.selectListByPathPrefix(root.getPath());
|
||||
descendants.forEach(d -> result.add(d.getId()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> listCodesByIds(Collection<Long> ids) {
|
||||
if (CollUtil.isEmpty(ids)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// 复用 getDeptList 已有的空判断与批量查询,过滤 code 为空的记录
|
||||
return getDeptList(ids).stream()
|
||||
.filter(d -> StrUtil.isNotBlank(d.getCode()))
|
||||
.map(DeptDO::getCode)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateDeptList(Collection<Long> ids) {
|
||||
if (CollUtil.isEmpty(ids)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.njcn.rdms.module.system.dal.dataobject.dept.OrgLeaderRelationDO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 组织负责人关系 Service
|
||||
@@ -49,4 +50,12 @@ public interface OrgLeaderRelationService {
|
||||
*/
|
||||
List<AdminUserDO> getCandidateUsersByDeptId(Long deptId);
|
||||
|
||||
/**
|
||||
* 查询指定用户当前生效的负责人关系所对应的 dept_id 集合
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @return 当前生效的组织 ID 集合,无关系时返回空集
|
||||
*/
|
||||
Set<Long> listEffectiveDeptIdsByUserId(Long userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertSet;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.*;
|
||||
import static java.util.Collections.singleton;
|
||||
@@ -68,6 +70,12 @@ public class OrgLeaderRelationServiceImpl implements OrgLeaderRelationService {
|
||||
return orgLeaderRelationMapper.selectListByDeptId(deptId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> listEffectiveDeptIdsByUserId(Long userId) {
|
||||
List<OrgLeaderRelationDO> relations = orgLeaderRelationMapper.selectEffectiveListByUserId(userId, LocalDateTime.now());
|
||||
return convertSet(relations, OrgLeaderRelationDO::getDeptId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AdminUserDO> getCandidateUsersByDeptId(Long deptId) {
|
||||
validateDeptExists(deptId);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.njcn.rdms.module.system.service.permission;
|
||||
|
||||
import com.njcn.rdms.module.system.controller.admin.permission.vo.userVisibilityConfig.UserVisibilityConfigSaveReqVO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.UserVisibilityConfigDO;
|
||||
|
||||
/**
|
||||
* 用户数据可见性配置 Service 接口
|
||||
*/
|
||||
public interface UserVisibilityConfigService {
|
||||
|
||||
/**
|
||||
* 按 userId 查询可见性配置(可为 null,表示该用户未配置)。
|
||||
*/
|
||||
UserVisibilityConfigDO getByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 保存或更新用户可见性配置。
|
||||
*
|
||||
* 一人一条配置(user_id 唯一索引):已有记录则 update,否则 insert。
|
||||
* 保存前校验 visibilityType 与字段的一致性:
|
||||
* - all → visibleDirectionIds / visibleProjectIds 均须为 null
|
||||
* - directions → visibleDirectionIds 非空;visibleProjectIds 须为 null
|
||||
* - projects → visibleProjectIds 非空;visibleDirectionIds 须为 null
|
||||
*
|
||||
* @return 配置记录 id
|
||||
*/
|
||||
Long saveOrUpdate(UserVisibilityConfigSaveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 按 userId 删除配置(唯一索引,无需按 id 删)。
|
||||
*/
|
||||
void deleteByUserId(Long userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.njcn.rdms.module.system.service.permission;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.system.controller.admin.permission.vo.userVisibilityConfig.UserVisibilityConfigSaveReqVO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.UserVisibilityConfigDO;
|
||||
import com.njcn.rdms.module.system.dal.mysql.permission.UserVisibilityConfigMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH;
|
||||
|
||||
/**
|
||||
* 用户数据可见性配置 Service 实现
|
||||
*/
|
||||
@Service
|
||||
public class UserVisibilityConfigServiceImpl implements UserVisibilityConfigService {
|
||||
|
||||
@Resource
|
||||
private UserVisibilityConfigMapper userVisibilityConfigMapper;
|
||||
|
||||
@Override
|
||||
public UserVisibilityConfigDO getByUserId(Long userId) {
|
||||
return userVisibilityConfigMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long saveOrUpdate(UserVisibilityConfigSaveReqVO reqVO) {
|
||||
// 校验 visibilityType 与字段的一致性
|
||||
validateTypeFieldConsistency(reqVO);
|
||||
|
||||
UserVisibilityConfigDO existing = userVisibilityConfigMapper.selectByUserId(reqVO.getUserId());
|
||||
if (existing == null) {
|
||||
// 不存在 → insert
|
||||
UserVisibilityConfigDO configDO = BeanUtils.toBean(reqVO, UserVisibilityConfigDO.class);
|
||||
userVisibilityConfigMapper.insert(configDO);
|
||||
return configDO.getId();
|
||||
} else {
|
||||
// 已存在 → update(覆盖全部字段)
|
||||
UserVisibilityConfigDO configDO = BeanUtils.toBean(reqVO, UserVisibilityConfigDO.class);
|
||||
configDO.setId(existing.getId());
|
||||
userVisibilityConfigMapper.updateById(configDO);
|
||||
return existing.getId();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByUserId(Long userId) {
|
||||
UserVisibilityConfigDO existing = userVisibilityConfigMapper.selectByUserId(userId);
|
||||
if (existing != null) {
|
||||
userVisibilityConfigMapper.deleteById(existing.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 visibilityType 与关联字段的一致性:
|
||||
* - all:directionIds / projectIds 均须为 null
|
||||
* - directions:directionIds 非空;projectIds 须为 null
|
||||
* - projects:projectIds 非空;directionIds 须为 null
|
||||
*/
|
||||
private void validateTypeFieldConsistency(UserVisibilityConfigSaveReqVO reqVO) {
|
||||
String type = reqVO.getVisibilityType();
|
||||
boolean hasDirectionIds = CollUtil.isNotEmpty(reqVO.getVisibleDirectionIds());
|
||||
boolean hasProjectIds = CollUtil.isNotEmpty(reqVO.getVisibleProjectIds());
|
||||
|
||||
switch (type) {
|
||||
case "all" -> {
|
||||
if (hasDirectionIds || hasProjectIds) {
|
||||
throw exception(USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH, type,
|
||||
"type=all 时,visibleDirectionIds 和 visibleProjectIds 均须为空");
|
||||
}
|
||||
}
|
||||
case "directions" -> {
|
||||
if (!hasDirectionIds) {
|
||||
throw exception(USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH, type,
|
||||
"type=directions 时,visibleDirectionIds 不能为空");
|
||||
}
|
||||
if (hasProjectIds) {
|
||||
throw exception(USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH, type,
|
||||
"type=directions 时,visibleProjectIds 须为空");
|
||||
}
|
||||
}
|
||||
case "projects" -> {
|
||||
if (!hasProjectIds) {
|
||||
throw exception(USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH, type,
|
||||
"type=projects 时,visibleProjectIds 不能为空");
|
||||
}
|
||||
if (hasDirectionIds) {
|
||||
throw exception(USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH, type,
|
||||
"type=projects 时,visibleDirectionIds 须为空");
|
||||
}
|
||||
}
|
||||
default -> throw exception(USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH, type,
|
||||
"不支持的 visibilityType,合法值:all / directions / projects");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 后台用户 Service 接口
|
||||
@@ -243,4 +244,12 @@ public interface AdminUserService {
|
||||
* @return 用户列表
|
||||
*/
|
||||
List<AdminUserDO> getAllUserByDeptId(Long deptId);
|
||||
|
||||
/**
|
||||
* 获得指定部门集合下启用且未离职的用户 ID 集合
|
||||
*
|
||||
* @param deptIds 部门 ID 集合
|
||||
* @return 可用用户 ID 集合
|
||||
*/
|
||||
Set<Long> listEnabledUserIdsByDeptIds(Collection<Long> deptIds);
|
||||
}
|
||||
|
||||
@@ -496,6 +496,15 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
&& !isUserResigned(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> listEnabledUserIdsByDeptIds(Collection<Long> deptIds) {
|
||||
List<AdminUserDO> users = getUserListByDeptIds(deptIds);
|
||||
return users.stream()
|
||||
.filter(this::isUserAvailable)
|
||||
.map(AdminUserDO::getId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AdminUserDO> getAllUserByDeptId(Long deptId) {
|
||||
Set<Long> deptCondition = getDeptCondition(deptId);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.njcn.rdms.module.system.api.dept;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.system.service.dept.DeptService;
|
||||
import com.njcn.rdms.module.system.service.dept.OrgLeaderRelationService;
|
||||
import com.njcn.rdms.module.system.service.user.AdminUserService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class OrgLeaderApiImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@Mock
|
||||
private OrgLeaderRelationService orgLeaderRelationService;
|
||||
|
||||
@Mock
|
||||
private DeptService deptService;
|
||||
|
||||
@Mock
|
||||
private AdminUserService adminUserService;
|
||||
|
||||
@InjectMocks
|
||||
private OrgLeaderApiImpl orgLeaderApi;
|
||||
|
||||
/** 用户没有任何生效的负责人关系,直接返回空集 */
|
||||
@Test
|
||||
void getReachableUserIds_returnsEmpty_whenUserIsNotLeader() {
|
||||
Long currentUserId = 1L;
|
||||
when(orgLeaderRelationService.listEffectiveDeptIdsByUserId(currentUserId))
|
||||
.thenReturn(Collections.emptySet());
|
||||
|
||||
CommonResult<Set<Long>> result = orgLeaderApi.getReachableUserIds(currentUserId);
|
||||
|
||||
assertTrue(result.getCheckedData().isEmpty());
|
||||
}
|
||||
|
||||
/** 叶子节点 leader:只有直属部门用户,无子孙部门 */
|
||||
@Test
|
||||
void getReachableUserIds_returnsDirectSubordinates_whenUserIsLeafLeader() {
|
||||
Long currentUserId = 10L;
|
||||
Long deptId = 100L;
|
||||
Long subordinateId = 20L;
|
||||
|
||||
when(orgLeaderRelationService.listEffectiveDeptIdsByUserId(currentUserId))
|
||||
.thenReturn(Set.of(deptId));
|
||||
// 叶子节点:listDescendantDeptIds 仅返回自身
|
||||
when(deptService.listDescendantDeptIds(Set.of(deptId)))
|
||||
.thenReturn(Set.of(deptId));
|
||||
when(adminUserService.listEnabledUserIdsByDeptIds(Set.of(deptId)))
|
||||
.thenReturn(new HashSet<>(Set.of(subordinateId)));
|
||||
|
||||
CommonResult<Set<Long>> result = orgLeaderApi.getReachableUserIds(currentUserId);
|
||||
|
||||
assertEquals(Set.of(subordinateId), result.getCheckedData());
|
||||
}
|
||||
|
||||
/** 父节点 leader:结果包含所有子孙部门的可用用户 */
|
||||
@Test
|
||||
void getReachableUserIds_returnsRecursiveSubordinates_whenUserIsParentLeader() {
|
||||
Long currentUserId = 10L;
|
||||
Long rootDeptId = 100L;
|
||||
Long childDeptId = 200L;
|
||||
Long user1 = 21L;
|
||||
Long user2 = 22L;
|
||||
|
||||
when(orgLeaderRelationService.listEffectiveDeptIdsByUserId(currentUserId))
|
||||
.thenReturn(Set.of(rootDeptId));
|
||||
// 父节点:listDescendantDeptIds 返回自身 + 子孙
|
||||
when(deptService.listDescendantDeptIds(Set.of(rootDeptId)))
|
||||
.thenReturn(Set.of(rootDeptId, childDeptId));
|
||||
when(adminUserService.listEnabledUserIdsByDeptIds(Set.of(rootDeptId, childDeptId)))
|
||||
.thenReturn(new HashSet<>(Set.of(user1, user2)));
|
||||
|
||||
CommonResult<Set<Long>> result = orgLeaderApi.getReachableUserIds(currentUserId);
|
||||
|
||||
assertEquals(Set.of(user1, user2), result.getCheckedData());
|
||||
}
|
||||
|
||||
/** 自己(currentUserId)即使在部门用户列表中,也不应出现在结果集里 */
|
||||
@Test
|
||||
void getReachableUserIds_excludesSelf() {
|
||||
Long currentUserId = 10L;
|
||||
Long deptId = 100L;
|
||||
Long otherId = 20L;
|
||||
|
||||
when(orgLeaderRelationService.listEffectiveDeptIdsByUserId(currentUserId))
|
||||
.thenReturn(Set.of(deptId));
|
||||
when(deptService.listDescendantDeptIds(Set.of(deptId)))
|
||||
.thenReturn(Set.of(deptId));
|
||||
// 返回集合包含 leader 自己
|
||||
when(adminUserService.listEnabledUserIdsByDeptIds(Set.of(deptId)))
|
||||
.thenReturn(new HashSet<>(Set.of(currentUserId, otherId)));
|
||||
|
||||
CommonResult<Set<Long>> result = orgLeaderApi.getReachableUserIds(currentUserId);
|
||||
|
||||
assertFalse(result.getCheckedData().contains(currentUserId), "结果集不能包含 currentUserId 自身");
|
||||
assertTrue(result.getCheckedData().contains(otherId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.njcn.rdms.module.system.api.permission;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO;
|
||||
import com.njcn.rdms.module.system.enums.permission.RoleCodeEnum;
|
||||
import com.njcn.rdms.module.system.service.permission.PermissionService;
|
||||
import com.njcn.rdms.module.system.service.permission.RoleService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PermissionApiImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@Mock
|
||||
private PermissionService permissionService;
|
||||
|
||||
@Mock
|
||||
private RoleService roleService;
|
||||
|
||||
@InjectMocks
|
||||
private PermissionApiImpl permissionApi;
|
||||
|
||||
@Test
|
||||
void isSuperAdmin_returnsTrue_whenUserHasSuperAdminRole() {
|
||||
Long userId = 1L;
|
||||
Set<Long> roleIds = Set.of(100L);
|
||||
when(permissionService.getUserRoleIdListByUserId(userId)).thenReturn(roleIds);
|
||||
when(roleService.hasAnySuperAdmin(roleIds)).thenReturn(true);
|
||||
|
||||
CommonResult<Boolean> result = permissionApi.isSuperAdmin(userId);
|
||||
|
||||
assertTrue(result.getCheckedData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isSuperAdmin_returnsFalse_otherwise() {
|
||||
Long userId = 2L;
|
||||
Set<Long> roleIds = Set.of(200L);
|
||||
when(permissionService.getUserRoleIdListByUserId(userId)).thenReturn(roleIds);
|
||||
when(roleService.hasAnySuperAdmin(roleIds)).thenReturn(false);
|
||||
|
||||
CommonResult<Boolean> result = permissionApi.isSuperAdmin(userId);
|
||||
|
||||
assertFalse(result.getCheckedData());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.njcn.rdms.module.system.api.permission;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.UserVisibilityConfigRespDTO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.UserVisibilityConfigDO;
|
||||
import com.njcn.rdms.module.system.service.dept.DeptService;
|
||||
import com.njcn.rdms.module.system.service.permission.UserVisibilityConfigService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class UserVisibilityConfigApiImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@Mock
|
||||
private UserVisibilityConfigService userVisibilityConfigService;
|
||||
|
||||
@Mock
|
||||
private DeptService deptService;
|
||||
|
||||
@InjectMocks
|
||||
private UserVisibilityConfigApiImpl userVisibilityConfigApi;
|
||||
|
||||
/** 用户无配置时返回 null data,不抛异常 */
|
||||
@Test
|
||||
void getConfig_returnsNull_whenNoConfig() {
|
||||
when(userVisibilityConfigService.getByUserId(1L)).thenReturn(null);
|
||||
|
||||
CommonResult<UserVisibilityConfigRespDTO> result = userVisibilityConfigApi.getConfig(1L);
|
||||
|
||||
assertNull(result.getCheckedData());
|
||||
}
|
||||
|
||||
/** type=all 时,directionCodes / projectIds 均不填充 */
|
||||
@Test
|
||||
void getConfig_returnsAllType_withoutDirectionsOrProjects() {
|
||||
UserVisibilityConfigDO cfg = buildDO(1L, "all", null, null);
|
||||
when(userVisibilityConfigService.getByUserId(1L)).thenReturn(cfg);
|
||||
|
||||
CommonResult<UserVisibilityConfigRespDTO> result = userVisibilityConfigApi.getConfig(1L);
|
||||
|
||||
UserVisibilityConfigRespDTO dto = result.getCheckedData();
|
||||
assertEquals("all", dto.getType());
|
||||
assertNull(dto.getDirectionCodes());
|
||||
assertNull(dto.getProjectIds());
|
||||
}
|
||||
|
||||
/** type=directions 时,directionIds 经 deptService.listCodesByIds 转换为 code 集合 */
|
||||
@Test
|
||||
void getConfig_returnsDirectionsTypeWithCodes() {
|
||||
List<Long> directionIds = List.of(101L, 102L);
|
||||
UserVisibilityConfigDO cfg = buildDO(2L, "directions", directionIds, null);
|
||||
when(userVisibilityConfigService.getByUserId(2L)).thenReturn(cfg);
|
||||
when(deptService.listCodesByIds(directionIds)).thenReturn(Set.of("dir_code_A", "dir_code_B"));
|
||||
|
||||
CommonResult<UserVisibilityConfigRespDTO> result = userVisibilityConfigApi.getConfig(2L);
|
||||
|
||||
UserVisibilityConfigRespDTO dto = result.getCheckedData();
|
||||
assertEquals("directions", dto.getType());
|
||||
assertEquals(Set.of("dir_code_A", "dir_code_B"), dto.getDirectionCodes());
|
||||
assertNull(dto.getProjectIds());
|
||||
}
|
||||
|
||||
/** type=projects 时,projectIds 直接透传到 DTO,不调用 deptService */
|
||||
@Test
|
||||
void getConfig_returnsProjectsType() {
|
||||
List<Long> projectIds = List.of(201L, 202L);
|
||||
UserVisibilityConfigDO cfg = buildDO(3L, "projects", null, projectIds);
|
||||
when(userVisibilityConfigService.getByUserId(3L)).thenReturn(cfg);
|
||||
|
||||
CommonResult<UserVisibilityConfigRespDTO> result = userVisibilityConfigApi.getConfig(3L);
|
||||
|
||||
UserVisibilityConfigRespDTO dto = result.getCheckedData();
|
||||
assertEquals("projects", dto.getType());
|
||||
assertNull(dto.getDirectionCodes());
|
||||
assertTrue(dto.getProjectIds().containsAll(Set.of(201L, 202L)));
|
||||
assertEquals(2, dto.getProjectIds().size());
|
||||
}
|
||||
|
||||
// ===== 辅助方法 =====
|
||||
|
||||
private static UserVisibilityConfigDO buildDO(Long userId, String type,
|
||||
List<Long> directionIds,
|
||||
List<Long> projectIds) {
|
||||
UserVisibilityConfigDO do_ = new UserVisibilityConfigDO();
|
||||
do_.setUserId(userId);
|
||||
do_.setVisibilityType(type);
|
||||
do_.setVisibleDirectionIds(directionIds);
|
||||
do_.setVisibleProjectIds(projectIds);
|
||||
return do_;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.njcn.rdms.module.system.service.permission;
|
||||
|
||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.system.controller.admin.permission.vo.userVisibilityConfig.UserVisibilityConfigSaveReqVO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.permission.UserVisibilityConfigDO;
|
||||
import com.njcn.rdms.module.system.dal.mysql.permission.UserVisibilityConfigMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class UserVisibilityConfigServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@Mock
|
||||
private UserVisibilityConfigMapper userVisibilityConfigMapper;
|
||||
|
||||
@InjectMocks
|
||||
private UserVisibilityConfigServiceImpl userVisibilityConfigService;
|
||||
|
||||
/** 用户无配置时执行 insert,返回新记录 id */
|
||||
@Test
|
||||
void saveOrUpdate_insertNew_whenUserHasNoConfig() {
|
||||
UserVisibilityConfigSaveReqVO reqVO = buildReqVO(1L, "all", null, null);
|
||||
when(userVisibilityConfigMapper.selectByUserId(1L)).thenReturn(null);
|
||||
|
||||
// insert 由 BaseMapper 填充 id,这里验证确实调了 insert(而非 updateById)
|
||||
userVisibilityConfigService.saveOrUpdate(reqVO);
|
||||
|
||||
verify(userVisibilityConfigMapper).insert(any(UserVisibilityConfigDO.class));
|
||||
verify(userVisibilityConfigMapper, never()).updateById(any(UserVisibilityConfigDO.class));
|
||||
}
|
||||
|
||||
/** 用户已有配置时执行 update,返回已有记录 id */
|
||||
@Test
|
||||
void saveOrUpdate_updateExisting_whenUserAlreadyHasConfig() {
|
||||
Long existingId = 999L;
|
||||
UserVisibilityConfigDO existing = buildDO(existingId, 2L, "all");
|
||||
UserVisibilityConfigSaveReqVO reqVO = buildReqVO(2L, "directions", List.of(101L, 102L), null);
|
||||
when(userVisibilityConfigMapper.selectByUserId(2L)).thenReturn(existing);
|
||||
|
||||
Long returnedId = userVisibilityConfigService.saveOrUpdate(reqVO);
|
||||
|
||||
assertEquals(existingId, returnedId);
|
||||
verify(userVisibilityConfigMapper).updateById(any(UserVisibilityConfigDO.class));
|
||||
verify(userVisibilityConfigMapper, never()).insert(any(UserVisibilityConfigDO.class));
|
||||
}
|
||||
|
||||
/** type=all 但传了 visibleDirectionIds,应抛类型字段不匹配异常 */
|
||||
@Test
|
||||
void saveOrUpdate_typeAllWithDirectionIds_shouldThrowMismatch() {
|
||||
UserVisibilityConfigSaveReqVO reqVO = buildReqVO(3L, "all", List.of(101L), null);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> userVisibilityConfigService.saveOrUpdate(reqVO));
|
||||
|
||||
assertEquals(USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH.getCode(), ex.getCode());
|
||||
verify(userVisibilityConfigMapper, never()).selectByUserId(any(Long.class));
|
||||
}
|
||||
|
||||
/** type=directions 但未传 visibleDirectionIds,应抛类型字段不匹配异常 */
|
||||
@Test
|
||||
void saveOrUpdate_typeDirectionsWithoutDirectionIds_shouldThrowMismatch() {
|
||||
UserVisibilityConfigSaveReqVO reqVO = buildReqVO(4L, "directions", null, null);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> userVisibilityConfigService.saveOrUpdate(reqVO));
|
||||
|
||||
assertEquals(USER_VISIBILITY_CONFIG_TYPE_FIELD_MISMATCH.getCode(), ex.getCode());
|
||||
verify(userVisibilityConfigMapper, never()).selectByUserId(any(Long.class));
|
||||
}
|
||||
|
||||
// ===== 辅助方法 =====
|
||||
|
||||
private static UserVisibilityConfigSaveReqVO buildReqVO(Long userId, String type,
|
||||
List<Long> directionIds,
|
||||
List<Long> projectIds) {
|
||||
UserVisibilityConfigSaveReqVO reqVO = new UserVisibilityConfigSaveReqVO();
|
||||
reqVO.setUserId(userId);
|
||||
reqVO.setVisibilityType(type);
|
||||
reqVO.setVisibleDirectionIds(directionIds);
|
||||
reqVO.setVisibleProjectIds(projectIds);
|
||||
return reqVO;
|
||||
}
|
||||
|
||||
private static UserVisibilityConfigDO buildDO(Long id, Long userId, String type) {
|
||||
UserVisibilityConfigDO config = new UserVisibilityConfigDO();
|
||||
config.setId(id);
|
||||
config.setUserId(userId);
|
||||
config.setVisibilityType(type);
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user