11 Commits

Author SHA1 Message Date
dk
5815f49a79 fix(system-boot_user): 增加用户昵称不能为空的后端校验。 2026-04-16 20:55:29 +08:00
dk
0c91f5deaa fix(system-api、boot): 给用户管理功能相关的各种需要company字段的类,新增company字段。 2026-04-16 20:29:36 +08:00
dk
67040aaf5d fix(UserManagementRelationxxx.java): 优化了一些细节,主要是汇报关系 -> 管理链路。 2026-04-15 20:56:58 +08:00
dk
8af6842809 fix(UserManagementRelationxxx.java): 优化了一些细节,主要是代码注释,带人关系 -> 汇报关系。 2026-04-15 20:48:17 +08:00
9384b2f502 feat(system): 取消角色superadmin能看到所有菜单的约定,改为实际配置实际显示 2026-04-14 18:58:26 +08:00
dk
07d07c8f5f feat(UserManagementRelationxxx.java): 改造带人关系树的构造代码。
feat(UserController.java): 新增/list-by-dept-id接口,根据部门ID获取该部门和下属部门的用户精简信息列表。
fix(AdminUserServiceImpl.java): 修复删除某用户(含批量删除)后,带人关系树构造错乱、加载不出来的问题。
2026-04-14 16:32:06 +08:00
dk
c3dd0c9802 fix(package-info.java): 增加包声明。 2026-04-13 13:44:02 +08:00
dk
21ca027f3b feat(user-management-relation): 完成带人关系后端接口(即直接管理) 2026-04-10 16:26:59 +08:00
dk
017beb1d5f 1.提交到本地 2026-04-07 11:21:18 +08:00
dk
09cba49a7d Merge remote-tracking branch 'origin/main' 2026-04-07 11:14:54 +08:00
dk
7e22f79b5f 1.修复当有用户使用某个角色时,该角色也可以被禁用的BUG
2.引入热部署依赖,配置开启热部署(热更新快捷键:Ctrl+F9)
2026-04-07 11:14:28 +08:00
42 changed files with 1816 additions and 78 deletions

212
AGENTS.md Normal file
View File

@@ -0,0 +1,212 @@
# AGENTS.md
## 适用范围
本说明适用于以 `C:\code\gitea\rdms\cn-rdms` 为根目录的整个仓库。
描述仓库现状时,以当前代码、当前配置、当前文档中可直接验证的事实为准;除非用户明确要求,不引入历史实现、过渡方案或已废弃模型来解释当前状态。
默认回答保持精简,优先给结论、改动点和必要风险,不做过多展开;如果存在你关心但未展开的细节,由你继续追问后再补充。
## 交互原则
- 默认先给执行方案,说明目标、涉及模块、预计改动点和验证方式。
- 在用户评审并明确同意前,不直接开始实际修改、编译、测试、打包或其他执行动作。
- 是否执行由用户决定;如果用户只要求分析、审阅或出方案,就停留在分析和方案层。
## 项目概览
这是一个面向 RDMS 服务的多模块 Maven 单仓库项目。
- Java 版本17
- 构建工具Maven
- 根模块打包类型:`pom`
- Spring Boot 版本:`3.5.9`
顶层模块:
1. `rdms-system`
2. `rdms-framework`
3. `rdms-gateway`
当前业务实现主要集中在 `rdms-system`,但这只是现阶段结构,不应被理解为长期只保留一个业务模块。
后续如果新增独立业务服务,例如项目/产品管理模块、工作流模块,应继续沿用当前仓库的模块拆分方式,而不是把所有后续业务长期堆进 `rdms-system`
## 模块说明
### `rdms-system`
当前已存在的系统业务聚合模块。
- `rdms-system/rdms-system-boot`
- 主应用模块
- 启动入口:`rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/SystemServerApplication.java`
- 主包路径:`com.njcn.rdms.module.system`
- 常见子包:`api``controller``convert``dal``framework``job``service``util``websocket`
- `rdms-system/rdms-system-api`
- 供其他服务依赖的共享 API 模块
- 包含对外 API 契约与枚举定义
说明:
- 当前权限、用户、组织、岗位、菜单、角色等系统核心能力主要落在这里。
- 如果后续只是给系统域补充新的系统子能力,可以继续在 `rdms-system` 内按现有结构扩展。
- 如果后续形成独立业务域,例如 `rdms-project``rdms-workflow`,应优先建设为新的独立业务模块,而不是默认继续塞进 `rdms-system`
### `rdms-framework`
共享框架与内部 starter 模块。
- `rdms-framework/rdms-common`
- 核心通用工具与公共抽象
- 其他 `rdms-spring-boot-starter-*` 模块
- 内部 starter覆盖 `env``web``rpc``security``mybatis``redis``mq``websocket``excel``protection``test``biz-ip`
### `rdms-gateway`
Spring Cloud Gateway 网关服务。
- 启动入口:`rdms-gateway/src/main/java/com/njcn/rdms/gateway/GatewayServerApplication.java`
- 主包路径:`com.njcn.rdms.gateway`
- 常见子包:`filter``handler``jackson``route``util`
## 模块演进约束
后续新增业务能力时,先区分下面两种情况,不要混用:
1. 新增独立微服务模块,例如 `rdms-project``rdms-workflow`
2. 只是在现有 `rdms-system` 中新增一个业务子域
### 新增独立微服务模块
如果后续能力已经具备独立服务边界,应优先按下面结构建设:
```text
rdms-xxx
├─ rdms-xxx-api
└─ rdms-xxx-boot
```
约束:
-`pom.xml` 增加新的聚合模块
- `api` 模块承载对外 RPC/Feign 接口、DTO、错误码、枚举、常量
- `boot` 模块承载启动类、controller、service、dal、convert、api 实现、模块级 framework 配置和资源文件
- 包路径、`spring.application.name``ApiConstants``RpcConstants``rdms.info.base-package` 必须保持一致
- 新服务不是简单复制 `rdms-system` 的名字,而是复用它的工程骨架和分层习惯
### 在 `rdms-system` 中新增业务子域
如果只是给系统服务补一个当前阶段仍适合放在 `rdms-system` 内的子域,则继续沿用现有结构:
- `controller/admin/...``controller/app/...`
- `service/...`
- `dal/dataobject/...`
- `dal/mysql/...`
- `convert/...`
- 需要跨模块暴露时,在 `rdms-system-api` 中补 API、DTO、错误码、枚举
约束:
- 不要为了新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层语言
- 不要让外部模块直接依赖 `rdms-system-boot` 的 service 或 mapper
- 如果某项能力未来明显会演进成独立微服务,文档和实现上都要避免把它写死成只能存在于 `rdms-system`
## 代码目录
- Java 源码:`*/src/main/java`
- 资源文件:`*/src/main/resources`
- 测试代码:`*/src/test/java`
- 本地辅助脚本:`scripts/`
## 分层职责约束
### `rdms-framework`
- `rdms-framework` 承担基础能力,不承载具体业务语义。
- 除非出现框架级缺陷,或该能力明确属于全局可复用基础设施,否则不要把业务判断硬塞进 framework。
### `rdms-gateway`
- `rdms-gateway` 只负责统一入口、令牌校验、登录用户透传、路由和网关层横切逻辑。
- 不要在 gateway 层承载组织、成员、负责人、项目、产品、工作流状态流转或数据可见性这类业务语义。
### Controller 层
- Controller 负责 HTTP 暴露、参数校验、权限注解、结果封装。
- 不要在 controller 中直接编排复杂业务流程,也不要直接操作多个 mapper 拼装业务规则。
- 请求和响应对象优先沿用 `ReqVO``RespVO` 风格,不要直接把 DO 暴露给前端。
### Service 层
- 核心业务规则、事务、缓存、领域编排应落在 service 层。
- 如果是已有领域增强,优先在现有 service 下扩展,不要为了“看起来更整齐”平移整套代码。
- 不要把复杂规则散落到 controller、mapper 或 `util` 中。
### DAL 层
- 新表应有对应的 DO 和 Mapper。
- Mapper 优先继承 `BaseMapperX<T>`,不要重复写样板 CRUD。
- 查询条件优先沿用 `LambdaQueryWrapperX`、默认方法封装和现有 MyBatis Plus 风格,不要无必要回退到 XML。
- Mapper 以查询封装为主,不承担领域校验职责。
### Convert 层
- 如果某个领域已经有 `convert` 风格,则继续沿用。
- 简单场景允许直接使用 `BeanUtils`
- 不要为了统一而强推所有地方都改成 MapStruct也不要反过来把已有 convert 全部删掉。
## 认证与共享调用约束
- 默认沿用现有 OAuth2 / Token / `LoginUser` / `login-user` 透传主链,不要另造一套认证上下文体系。
- 不要额外发明 ThreadLocal、Session 或自定义 header 体系替代当前登录态恢复方式。
- 接口级权限判断默认沿用 `@PreAuthorize("@ss.hasPermission(...)")` 这条链路,不要绕开现有权限框架另起一套实现。
- 跨模块、跨服务访问能力时,优先通过对应的 `*-api` 模块定义 API、DTO、常量和枚举。
- 不要让外部模块直接依赖某个 `*-boot` 模块的 service 或 mapper。
## 数据与 SQL 约束
- 新增业务表的 DO 优先复用当前 `BaseDO` / 审计字段风格;除非表本身明确不需要逻辑删除,不要再引入另一套审计基类。
- 不要假设运行时存在自动数据库迁移;如果代码依赖新表、新字段或新索引,必须同步补齐对应 SQL 与文档说明。
- SQL 脚本应放在目标模块的 `src/main/resources/sql/...` 下,并保持可审阅、可单独执行、语义清晰。
- 变更缓存、日志、审计相关逻辑时,优先沿用现有机制,不要绕开现有登录上下文、缓存约定和审计字段填充方式。
## 注释与编码
- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。
- 不要为了省事删除原有有效注释,也不要添加无信息量的注释。
- 写入中文内容时必须保持 UTF-8 编码,并自行检查中文显示是否正常;不要用“改成英文”规避乱码问题。
## 工作规则
1. 除非任务明确要求修改共享契约或 starter否则优先进行有边界的模块内改动避免跨模块扩散。
2. 业务逻辑应放在对应业务模块的 `*-boot` 实现模块;可复用契约放在对应的 `*-api` 模块;可复用框架能力放在 `rdms-framework`
3. 除非任务本身就是环境配置调整,否则避免修改 `application-local.yaml``application-dev.yaml`
4. 将本地资源 YAML 视为可能带有机器环境差异的文件;修改前先检查 git 状态。
5. 保持既有包结构约定不变:
- 控制器放在 `controller`
- 服务层放在 `service`
- 持久层放在 `dal`
- DTO/VO 转换放在 `convert`
6. 当前业务代码主要在 `rdms-system`,但这不是永久约束;新增业务能力时,先判断应该落在现有系统域内,还是应建设为新的 `rdms-xxx` 业务模块。
7. 新增共享能力时,优先扩展现有 `rdms-spring-boot-starter-*` 模块,不要在业务服务里重复堆配置。
8. 修改跨模块使用的 API 时,需要同时更新提供方实现和对应的 `rdms-system-api` 或对应 `rdms-xxx-api` 契约。
9. 除非用户明确要求,否则不执行任何编译、构建、测试、打包或其他会实际运行项目的命令,包括但不限于 `mvn`、启动命令和脚本。
## 测试指引
先定义验证方式,再实施修改。默认通过以下方式验证:
- 代码路径是否闭环,调用链是否与模块边界一致
- 配置项、接口契约、权限标识、路由或资源注册是否前后一致
- 改动范围是否控制在当前任务所需的最小集合内
- 受影响的文档、SQL、配置或接口说明是否需要同步更新
如果任务影响了 Spring 配置、序列化、安全、路由、RPC 契约、MyBatis 行为或跨模块 API一律明确说明哪些部分已静态检查、哪些部分尚未实际运行验证。
## 给后续 Agent 的说明
- 仓库中可能存在未提交的本地配置改动,不要覆盖与当前任务无关的编辑。
- `docs/` 目录属于当前工作上下文的一部分,不是归档材料;做架构级修改前先查阅。
- 根目录 `pom.xml` 负责统一版本和依赖对齐;涉及版本调整时,优先修改根 `pom.xml`,不要散落到子模块中。

View File

@@ -118,6 +118,15 @@
<artifactId>rdms-spring-boot-starter-websocket</artifactId>
<version>${revision}</version>
</dependency>
<!-- 热部署依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>${spring.boot.version}</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -0,0 +1 @@
package com.njcn.rdms.framework.env.core;

View File

@@ -6,10 +6,10 @@ spring:
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 监控相关配置 ####################

View File

@@ -6,10 +6,10 @@ spring:
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 监控相关配置 ####################

View File

@@ -0,0 +1,49 @@
package com.njcn.rdms.module.system.api.user;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.collection.CollectionUtils;
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
import com.njcn.rdms.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@FeignClient(name = ApiConstants.NAME)
@Tag(name = "RPC 服务 - 用户管理链路")
public interface UserManagementRelationApi {
String PREFIX = ApiConstants.PREFIX + "/user-management-relation";
@GetMapping(PREFIX + "/list-by-manager")
@Operation(summary = "根据管理者用户ID获得管理链路列表")
@Parameter(name = "managerUserId", description = "管理者用户ID", example = "1", required = true)
CommonResult<List<UserManagementRelationRespDTO>> getRelationListByManagerUserId(@RequestParam("managerUserId") Long managerUserId);
@GetMapping(PREFIX + "/list-by-subordinate")
@Operation(summary = "根据被管理者用户ID获得管理链路列表")
@Parameter(name = "subordinateUserId", description = "被管理者用户ID", example = "2", required = true)
CommonResult<List<UserManagementRelationRespDTO>> getRelationListBySubordinateUserId(@RequestParam("subordinateUserId") Long subordinateUserId);
@GetMapping(PREFIX + "/list")
@Operation(summary = "获得管理链路列表")
@Parameter(name = "ids", description = "关系编号数组", example = "1,2", required = true)
CommonResult<List<UserManagementRelationRespDTO>> getRelationList(@RequestParam("ids") Collection<Long> ids);
default Map<Long, UserManagementRelationRespDTO> getRelationMap(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return MapUtil.empty();
}
List<UserManagementRelationRespDTO> list = getRelationList(ids).getData();
return CollectionUtils.convertMap(list, UserManagementRelationRespDTO::getId);
}
}

View File

@@ -14,6 +14,9 @@ public class AdminUserRespDTO implements VO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王")
private String nickname;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "帐号状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status; // 参见 CommonStatusEnum 枚举

View File

@@ -0,0 +1,35 @@
package com.njcn.rdms.module.system.api.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户管理链路 Response DTO
*
* @author dklive
*/
@Schema(description = "RPC 服务 - 用户管理链路 Response DTO")
@Data
public class UserManagementRelationRespDTO {
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "管理者用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long managerUserId;
@Schema(description = "被管理用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long subordinateUserId;
@Schema(description = "生效开始时间")
private LocalDateTime effectiveFrom;
@Schema(description = "生效结束时间")
private LocalDateTime effectiveUntil;
@Schema(description = "备注")
private String remark;
}

View File

@@ -39,6 +39,7 @@ public interface ErrorCodeConstants {
ErrorCode ROLE_CAN_NOT_DELETE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能删除类型为系统内置的角色");
ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用");
ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "标识【{}】不能使用");
ErrorCode ROLE_DISABLE_NOT_ALLOWED = new ErrorCode(1_002_005_006, "该角色还有用户在使用,不允许禁用");
// ========== 用户模块 1-002-003-000 ==========
ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在");
@@ -53,6 +54,11 @@ public interface ErrorCodeConstants {
ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭");
ErrorCode USER_IS_RESIGNED = new ErrorCode(1_002_003_012, "名字为【{}】的用户已离职");
// ========== 用户管理链路模块 1-002-003-100 ==========
ErrorCode USER_MANAGEMENT_RELATION_NOT_FOUND = new ErrorCode(1_002_003_100, "用户管理链路不存在");
ErrorCode USER_MANAGEMENT_RELATION_MANAGER_EXISTS = new ErrorCode(1_002_003_101, "该用户已有直属上级,不能重复添加");
ErrorCode USER_MANAGEMENT_RELATION_EXISTS = new ErrorCode(1_002_003_102, "该用户在管理链路中还在使用,不可删除!");
// ========== 部门模块 1-002-004-000 ==========
ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");
ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在");
@@ -70,6 +76,7 @@ public interface ErrorCodeConstants {
ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1_002_005_002, "已经存在该名字的岗位");
ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1_002_005_003, "已经存在该标识的岗位");
ErrorCode POST_TYPE_INVALID = new ErrorCode(1_002_005_004, "岗位类型({})不合法");
ErrorCode POST_DISABLE_NOT_ALLOWED = new ErrorCode(1_002_005_005, "该岗位还有用户在使用,不允许禁用");
// ========== 字典类型 1-002-006-000 ==========
ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1_002_006_001, "当前字典类型不存在");

View File

@@ -31,8 +31,6 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>rdms-spring-boot-starter-biz-ip</artifactId>
@@ -79,8 +77,6 @@
<artifactId>rdms-spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>com.njcn</groupId>
@@ -125,6 +121,13 @@
<artifactId>s3</artifactId>
</dependency>
<!-- 热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
@@ -136,6 +139,9 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<configuration>
<addResources>true</addResources> <!-- 开启热部署必须配置 -->
</configuration>
<executions>
<execution>
<goals>

View File

@@ -0,0 +1,48 @@
package com.njcn.rdms.module.system.api.user;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
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.Collection;
import java.util.Collections;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@RestController
@Validated
@Hidden
public class UserManagementRelationApiImpl implements UserManagementRelationApi {
@Resource
private UserManagementRelationService userManagementRelationService;
@Override
public CommonResult<List<UserManagementRelationRespDTO>> getRelationListByManagerUserId(Long managerUserId) {
List<UserManagementRelationDO> list = userManagementRelationService.getRelationListByManagerUserId(managerUserId);
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
}
@Override
public CommonResult<List<UserManagementRelationRespDTO>> getRelationListBySubordinateUserId(Long subordinateUserId) {
List<UserManagementRelationDO> list = userManagementRelationService.getRelationListBySubordinateUserId(subordinateUserId);
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
}
@Override
public CommonResult<List<UserManagementRelationRespDTO>> getRelationList(Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {
return success(Collections.emptyList());
}
List<UserManagementRelationDO> list = userManagementRelationService.getRelationList(ids);
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
}
}

View File

@@ -41,6 +41,9 @@ public class AuthPermissionInfoRespVO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能源码")
private String nickname;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.jpg")
private String avatar;

View File

@@ -24,6 +24,10 @@ public class AuthRegisterReqVO extends CaptchaVerificationReqVO {
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
private String nickname;
@Schema(description = "所属公司", example = "灿能")
@Size(max = 100, message = "所属公司长度不能超过 100 个字符")
private String company;
@Schema(description = "所属部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "所属部门不能为空")
private Long deptId;

View File

@@ -21,6 +21,9 @@ public class AuthUserInfoRespVO {
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
private String userName;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "角色编码列表", example = "[\"SUPER_ADMIN\"]")
private List<String> roles;

View File

@@ -12,7 +12,9 @@ import com.njcn.rdms.module.system.controller.admin.dict.vo.data.DictDataRespVO;
import com.njcn.rdms.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO;
import com.njcn.rdms.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO;
import com.njcn.rdms.module.system.dal.dataobject.dict.DictDataDO;
import com.njcn.rdms.module.system.dal.dataobject.dict.DictTypeDO;
import com.njcn.rdms.module.system.service.dict.DictDataService;
import com.njcn.rdms.module.system.service.dict.DictTypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -38,6 +40,9 @@ public class DictDataController {
@Resource
private DictDataService dictDataService;
@Resource
private DictTypeService dictTypeService;
@PostMapping("/create")
@Operation(summary = "新增字典数据")
@PreAuthorize("@ss.hasPermission('system:dict:create')")
@@ -98,6 +103,16 @@ public class DictDataController {
return success(BeanUtils.toBean(dictData, DictDataRespVO.class));
}
@GetMapping(value = "/code")
@Operation(summary = "/通过字典编码去查询字典数据详细")
@Parameter(name = "code", description = "编号", required = true, example = "system_user_company")
@PreAuthorize("@ss.hasPermission('system:dict:query')")
public CommonResult<List<DictDataRespVO>> getDictData(@RequestParam("code") String code) {
DictTypeDO dictType = dictTypeService.getDictType(code);
List<DictDataDO> dictDataList = dictDataService.getDictDataList(0, dictType.getType());
return success(BeanUtils.toBean(dictDataList, DictDataRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出字典数据")
@PreAuthorize("@ss.hasPermission('system:dict:export')")

View File

@@ -20,6 +20,9 @@ public class OAuth2UserInfoRespVO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能")
private String nickname;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "用户邮箱", example = "rdms@iocoder.cn")
private String email;

View File

@@ -141,6 +141,15 @@ public class UserController {
return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap));
}
@GetMapping("/list-by-dept-id")
@Operation(summary = "根据部门ID获取该部门和下属部门的用户精简信息列表不分页")
@Parameter(name = "deptId", description = "部门ID", required = true)
public CommonResult<List<UserSimpleRespVO>> getUserListByDeptId(@RequestParam("deptId") Long deptId) {
List<AdminUserDO> list = userService.getAllUserByDeptId(deptId);
Map<Long, DeptDO> deptMap = deptService.getDeptMap(convertList(list, AdminUserDO::getDeptId));
return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap));
}
@GetMapping("/get")
@Operation(summary = "获得用户详情")
@Parameter(name = "id", description = "编号", required = true, example = "1024")

View File

@@ -0,0 +1,191 @@
package com.njcn.rdms.module.system.controller.admin.user;
import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationQueryReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationRespVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationSaveReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationTreeRespVO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
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.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
/**
* 用户管理链路 Controller
*
* 提供用户管理链路的管理接口,包括:
* - 创建、更新、删除用户管理链路
* - 查询用户管理链路列表和详情
* - 获取用户管理链路树形结构
* - 导出用户管理链路数据
*
* @author dklive
*/
@Tag(name = "管理后台 - 用户管理链路")
@RestController
@RequestMapping("/system/user-management-relation")
@Validated
public class UserManagementRelationController {
@Resource
private UserManagementRelationService userManagementRelationService;
/**
* 创建用户管理链路
*
* 权限要求system:user-management-relation:create
*
* @param createReqVO 创建请求VO
* @return 关系记录主键ID
*/
@PostMapping("/create")
@Operation(summary = "创建用户管理链路")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:create')")
public CommonResult<Long> createUserManagementRelation(@Valid @RequestBody UserManagementRelationSaveReqVO createReqVO) {
Long id = userManagementRelationService.createRelation(createReqVO);
return success(id);
}
/**
* 修改用户管理链路
*
* 权限要求system:user-management-relation:update
*
* @param updateReqVO 更新请求VO
* @return 操作结果
*/
@PutMapping("/update")
@Operation(summary = "修改用户管理链路")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:update')")
public CommonResult<Boolean> updateUserManagementRelation(@Valid @RequestBody UserManagementRelationSaveReqVO updateReqVO) {
userManagementRelationService.updateRelation(updateReqVO);
return success(true);
}
/**
* 删除用户管理链路
*
* 根据主键ID删除单条用户管理链路记录
* 权限要求system:user-management-relation:delete
*
* @param id 关系记录主键ID
* @return 操作结果
*/
@DeleteMapping("/delete")
@Operation(summary = "删除用户管理链路")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:delete')")
public CommonResult<Boolean> deleteUserManagementRelation(@RequestParam("id") Long id) {
userManagementRelationService.deleteRelation(id);
return success(true);
}
/**
* 批量删除用户管理链路
*
* 根据主键ID列表批量删除用户管理链路记录
* 权限要求system:user-management-relation:delete
*
* @param ids 关系记录主键ID列表
* @return 操作结果
*/
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除用户管理链路")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:delete')")
public CommonResult<Boolean> deleteUserManagementRelationList(@RequestParam("ids") List<Long> ids) {
userManagementRelationService.deleteRelationList(ids);
return success(true);
}
/**
* 获得用户管理链路信息
*
* 根据主键ID查询单条用户管理链路记录
* 权限要求system:user-management-relation:query
*
* @param id 关系记录主键ID
* @return 用户管理链路详情
*/
@GetMapping(value = "/get")
@Operation(summary = "获得用户管理链路信息")
@Parameter(name = "id", description = "关系编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
public CommonResult<UserManagementRelationRespVO> getUserManagementRelation(@RequestParam("id") Long id) {
UserManagementRelationDO relation = userManagementRelationService.getRelation(id);
return success(BeanUtils.toBean(relation, UserManagementRelationRespVO.class));
}
/**
* 获取用户管理链路列表
*
* 根据查询条件查询用户管理链路记录列表
* 权限要求system:user-management-relation:query
*
* @param reqVO 查询条件VO
* @return 用户管理链路列表
*/
@GetMapping("/query")
@Operation(summary = "获取用户管理链路列表")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
public CommonResult<List<UserManagementRelationTreeRespVO>> getUserManagementRelationQuery(@Validated UserManagementRelationQueryReqVO reqVO) {
List<UserManagementRelationTreeRespVO> list = userManagementRelationService.getRelationQuery(reqVO);
return success(list);
}
/**
* 获取用户管理链路树形结构
*
* 构建用户上下级关系的树形结构,用于前端树形控件展示
* 树形结构特点:
* - 根节点:最高领导,没有上级
* - 中间节点:有上级也有下级
* - 叶子节点:基层员工,没有下级
*
* 权限要求system:user-management-relation:query
*
* @return 用户管理链路树形列表
*/
@GetMapping("/tree")
@Operation(summary = "获取用户管理链路树形结构", description = "用于前端树形控件展示,包含用户的上下级层级关系")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
public CommonResult<List<UserManagementRelationTreeRespVO>> getUserManagementRelationTree(@Validated UserManagementRelationQueryReqVO reqVO) {
return success(userManagementRelationService.getRelationTree(reqVO));
}
/**
* 导出用户管理链路 Excel
*
* 根据查询条件导出用户管理链路数据到Excel文件
* 权限要求system:user-management-relation:export
*
* @param response HTTP响应对象
* @param reqVO 查询条件VO
* @throws IOException IO异常
*/
@GetMapping("/export-excel")
@Operation(summary = "导出用户管理链路 Excel")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:export')")
@ApiAccessLog(operateType = EXPORT)
public void export(HttpServletResponse response, @Validated UserManagementRelationQueryReqVO reqVO) throws IOException {
List<UserManagementRelationTreeRespVO> list = userManagementRelationService.getRelationQuery(reqVO);
ExcelUtils.write(response, "用户管理链路数据.xls", "用户管理链路列表", UserManagementRelationRespVO.class,
BeanUtils.toBean(list, UserManagementRelationRespVO.class));
}
}

View File

@@ -22,6 +22,9 @@ public class UserProfileRespVO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
private String nickname;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "用户邮箱", example = "rdms@iocoder.cn")
private String email;

View File

@@ -24,6 +24,9 @@ public class UserImportExcelVO {
@ExcelProperty("用户名称")
private String nickname;
@ExcelProperty("所属公司")
private String company;
@ExcelProperty("部门编号")
private Long deptId;

View File

@@ -25,6 +25,9 @@ public class UserPageReqVO extends PageParam {
@Schema(description = "手机号码,模糊匹配", example = "rdms")
private String mobile;
@Schema(description = "所属公司,模糊匹配", example = "灿能")
private String company;
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
private Integer status;

View File

@@ -30,6 +30,10 @@ public class UserRespVO {
@Schema(description = "备注", example = "我是一个用户")
private String remark;
@Schema(description = "所属公司", example = "灿能")
@ExcelProperty("所属公司")
private String company;
@Schema(description = "部门编号", example = "1")
private Long deptId;

View File

@@ -33,7 +33,8 @@ public class UserSaveReqVO {
@DiffLogField(name = "用户账号")
private String username;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "戴坤")
@NotBlank(message = "用户昵称不能为空")
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
@DiffLogField(name = "用户昵称")
private String nickname;
@@ -42,6 +43,11 @@ public class UserSaveReqVO {
@DiffLogField(name = "备注")
private String remark;
@Schema(description = "所属公司", example = "灿能")
@Size(max = 100, message = "所属公司长度不能超过 100 个字符")
@DiffLogField(name = "所属公司")
private String company;
@Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "部门不能为空")
@DiffLogField(name = "部门", function = DeptParseFunction.NAME)

View File

@@ -0,0 +1,21 @@
package com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 用户管理链路列表 Request VO")
@Data
public class UserManagementRelationQueryReqVO {
@Schema(description = "管理者用户ID", example = "1")
private Long managerUserId;
@Schema(description = "被管理用户ID", example = "2")
private Long subordinateUserId;
@Schema(description = "访问是否来自user/index组件", example = "true/false")
private Boolean fromUserIndex;
@Schema(description = "所选中的部门id", example = "100灿能电力的部门id")
private Long deptId;
}

View File

@@ -0,0 +1,42 @@
package com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 用户管理链路信息 Response VO")
@Data
@ExcelIgnoreUnannotated
public class UserManagementRelationRespVO {
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("主键ID")
private Long id;
@Schema(description = "管理者用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty("管理者用户ID")
private Long managerUserId;
@Schema(description = "被管理用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("被管理用户ID")
private Long subordinateUserId;
@Schema(description = "生效开始时间")
@ExcelProperty("生效开始时间")
private LocalDateTime effectiveFrom;
@Schema(description = "生效结束时间")
@ExcelProperty("生效结束时间")
private LocalDateTime effectiveUntil;
@Schema(description = "备注", example = "直属上级关系")
@ExcelProperty("备注")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,33 @@
package com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 用户管理链路创建/修改 Request VO")
@Data
public class UserManagementRelationSaveReqVO {
@Schema(description = "主键ID", example = "1024")
private Long id;
@Schema(description = "管理者用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "管理者用户ID不能为空")
private Long managerUserId;
@Schema(description = "被管理用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "被管理用户ID不能为空")
private Long subordinateUserId;
@Schema(description = "生效开始时间")
private LocalDateTime effectiveFrom;
@Schema(description = "生效结束时间")
private LocalDateTime effectiveUntil;
@Schema(description = "备注", example = "直属上级关系")
private String remark;
}

View File

@@ -0,0 +1,60 @@
package com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 用户管理链路树形 Response VO
*
* 用于前端树形控件展示用户的上下级层级关系
* 包含关系记录的主键ID便于前端执行删除和更新操作
*
* @author hongawen
*/
@Schema(description = "管理后台 - 用户管理链路树形 Response VO")
@Data
public class UserManagementRelationTreeRespVO {
/**
* 关系记录主键ID
* 用于前端执行删除和更新操作
*/
@Schema(description = "关系记录主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
/**
* 用户ID
*/
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long userId;
/**
* 用户昵称
*/
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
private String userNickname;
/**
* 上级用户ID
* 最高领导此字段为null
*/
@Schema(description = "上级用户ID", example = "1")
private Long managerUserId;
/**
* 上级用户昵称
* 最高领导此字段为null
*/
@Schema(description = "上级用户昵称", example = "李四")
private String managerNickname;
/**
* 下级用户列表
* 基层员工此字段为空列表
*/
@Schema(description = "下级用户列表")
private List<UserManagementRelationTreeRespVO> children;
}

View File

@@ -57,6 +57,7 @@ public interface AuthConvert {
return AuthUserInfoRespVO.builder()
.userId(String.valueOf(user.getId()))
.userName(user.getUsername())
.company(user.getCompany())
.roles(sortDistinctStrings(convertList(roleList, RoleDO::getCode)))
.buttons(sortDistinctStrings(convertList(menuList, MenuDO::getPermission,
menu -> StrUtil.isNotBlank(menu.getPermission()))))

View File

@@ -57,6 +57,11 @@ public class AdminUserDO extends BaseDO {
*/
private String remark;
/**
* 所属公司
*/
private String company;
/**
* 部门 ID
*/

View File

@@ -0,0 +1,77 @@
package com.njcn.rdms.module.system.dal.dataobject.user;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 用户管理链路表 DO
*
* 用于存储用户之间的直属上下级管理关系
* 每条记录代表一个管理者与被管理者之间的关系
*
* 表名system_user_management_relation
*
* 业务场景:
* - 组织架构中的直属上下级关系管理
* - 支持关系的生效时间范围设置
* - 一个用户可以有多个上级,但只有一个直属上级
* - 一个用户可以有多个下属
*
* @author dklive
*/
@TableName("system_user_management_relation")
@Data
@EqualsAndHashCode(callSuper = true)
public class UserManagementRelationDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 管理者用户ID
*
* 表示上级用户的ID即管理者的用户ID
* 对应 system_users 表的 id 字段
*/
private Long managerUserId;
/**
* 被管理用户ID
*
* 表示下级用户的ID即被管理者的用户ID
* 对应 system_users 表的 id 字段
*/
private Long subordinateUserId;
/**
* 生效开始时间
*
* 关系开始生效的时间
* 为空表示立即长期生效
*/
private LocalDateTime effectiveFrom;
/**
* 生效结束时间
*
* 关系失效的时间
* 为空表示长期有效
*/
private LocalDateTime effectiveUntil;
/**
* 备注
*
* 用于记录关系的额外说明信息
*/
private String remark;
}

View File

@@ -29,6 +29,7 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>()
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
.likeIfPresent(AdminUserDO::getCompany, reqVO.getCompany())
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(AdminUserDO::getDeptId, deptIds)

View File

@@ -0,0 +1,76 @@
package com.njcn.rdms.module.system.dal.mysql.user;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationQueryReqVO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户管理链路 Mapper 接口
*
* 提供用户管理链路表的数据访问操作
* 继承 BaseMapperX 获得基础的 CRUD 功能
*
* @author hongawen
*/
@Mapper
public interface UserManagementRelationMapper extends BaseMapperX<UserManagementRelationDO> {
/**
* 根据查询条件查询用户管理链路列表
*
* 支持的查询条件:
* - managerUserId管理者用户ID精确匹配
* - subordinateUserId被管理用户ID精确匹配
*
* 排序规则按主键ID降序排列
*
* @param reqVO 查询条件VO
* @return 用户管理链路DO列表
*/
default List<UserManagementRelationDO> selectList(UserManagementRelationQueryReqVO reqVO) {
LocalDateTime now = LocalDateTime.now();
return selectList(new LambdaQueryWrapperX<UserManagementRelationDO>()
.eqIfPresent(UserManagementRelationDO::getManagerUserId, reqVO.getManagerUserId())
.eqIfPresent(UserManagementRelationDO::getSubordinateUserId, reqVO.getSubordinateUserId())
.orderByDesc(UserManagementRelationDO::getId)
// (from IS NULL OR from <= now)
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveFrom)
.or().le(UserManagementRelationDO::getEffectiveFrom, now))
// (until IS NULL OR until >= now)
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveUntil)
.or().ge(UserManagementRelationDO::getEffectiveUntil, now))
);
}
/**
* 根据管理者用户ID查询其下属关系列表
*
* 查询指定用户作为管理者时的所有管理链路记录
* 用于获取某个用户的所有直接下属
*
* @param managerUserId 管理者用户ID
* @return 用户管理链路DO列表
*/
default List<UserManagementRelationDO> selectListByManagerUserId(Long managerUserId) {
return selectList(UserManagementRelationDO::getManagerUserId, managerUserId);
}
/**
* 根据被管理者用户ID查询其上级关系列表
*
* 查询指定用户作为被管理者时的所有管理链路记录
* 用于获取某个用户的所有直接上级
*
* @param subordinateUserId 被管理者用户ID
* @return 用户管理链路DO列表
*/
default List<UserManagementRelationDO> selectListBySubordinateUserId(Long subordinateUserId) {
return selectList(UserManagementRelationDO::getSubordinateUserId, subordinateUserId);
}
}

View File

@@ -2,13 +2,16 @@ package com.njcn.rdms.module.system.service.dept;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.controller.admin.dept.vo.post.PostPageReqVO;
import com.njcn.rdms.module.system.controller.admin.dept.vo.post.PostSaveReqVO;
import com.njcn.rdms.module.system.dal.dataobject.dept.PostDO;
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
import com.njcn.rdms.module.system.dal.mysql.dept.PostMapper;
import com.njcn.rdms.module.system.dal.mysql.user.AdminUserMapper;
import com.njcn.rdms.module.system.enums.dept.PostTypeEnum;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
@@ -36,6 +39,9 @@ public class PostServiceImpl implements PostService {
@Resource
private PostMapper postMapper;
@Resource
private AdminUserMapper userMapper;
@Override
public Long createPost(PostSaveReqVO createReqVO) {
// 校验正确性
@@ -52,6 +58,10 @@ public class PostServiceImpl implements PostService {
// 校验正确性
validatePostForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getName(), updateReqVO.getCode(), updateReqVO.getPostType());
//如果前端想要禁用,则去校验能否被禁用
if (updateReqVO.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) {
VerifyDoDisable(updateReqVO.getId());
}
// 更新岗位
PostDO updateObj = BeanUtils.toBean(updateReqVO, PostDO.class);
postMapper.updateById(updateObj);
@@ -130,6 +140,41 @@ public class PostServiceImpl implements PostService {
}
}
private void VerifyDoDisable(Long id) {
/*
通过岗位id去检查是否有用户在使用该岗位position_id = id
1.只查deleted = 0的即没被删除的用户
2.无论用户是被禁用还是正常的status = 0 | 1只要有用户使用该岗位则不能被禁用
*/
QueryWrapper<AdminUserDO> wrapper = new QueryWrapper<>();
wrapper.eq("deleted", false)
.eq("position_id", id);
Long res = userMapper.selectCount(wrapper);
if (res > 0) {
throw exception(POST_DISABLE_NOT_ALLOWED);
}
}
@Override
public void validatePostList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
// 获得岗位信息
List<PostDO> posts = postMapper.selectByIds(ids);
Map<Long, PostDO> postMap = convertMap(posts, PostDO::getId);
// 校验
ids.forEach(id -> {
PostDO post = postMap.get(id);
if (post == null) {
throw exception(POST_NOT_FOUND);
}
if (!CommonStatusEnum.ENABLE.getStatus().equals(post.getStatus())) {
throw exception(POST_NOT_ENABLE, post.getName());
}
});
}
@Override
public List<PostDO> getPostList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
@@ -152,24 +197,4 @@ public class PostServiceImpl implements PostService {
public PostDO getPost(Long id) {
return postMapper.selectById(id);
}
@Override
public void validatePostList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
// 获得岗位信息
List<PostDO> posts = postMapper.selectByIds(ids);
Map<Long, PostDO> postMap = convertMap(posts, PostDO::getId);
// 校验
ids.forEach(id -> {
PostDO post = postMap.get(id);
if (post == null) {
throw exception(POST_NOT_FOUND);
}
if (!CommonStatusEnum.ENABLE.getStatus().equals(post.getStatus())) {
throw exception(POST_NOT_ENABLE, post.getName());
}
});
}
}

View File

@@ -223,11 +223,7 @@ public class PermissionServiceImpl implements PermissionService {
return Collections.emptySet();
}
// 如果是管理员的情况下,获取全部菜单编号
if (roleService.hasAnySuperAdmin(roleIds)) {
return convertSet(menuService.filterDisableMenus(menuService.getMenuList()), MenuDO::getId);
}
// 如果是非管理员的情况下,仅返回当前仍然有效的菜单,并补齐其父链
// 统一按角色实际授权返回当前仍然有效的菜单,并补齐其父链
Set<Long> menuIds = convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId);
List<MenuDO> menus = menuService.filterDisableMenus(menuService.getMenuList(menuIds));
return expandMenuIdsWithAncestors(convertSet(menus, MenuDO::getId));

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.annotations.VisibleForTesting;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
@@ -16,7 +17,9 @@ import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RolePageReqVO;
import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO;
import com.njcn.rdms.module.system.dal.dataobject.permission.UserRoleDO;
import com.njcn.rdms.module.system.dal.mysql.permission.RoleMapper;
import com.njcn.rdms.module.system.dal.mysql.permission.UserRoleMapper;
import com.njcn.rdms.module.system.dal.redis.RedisKeyConstants;
import com.njcn.rdms.module.system.enums.permission.RoleCodeEnum;
import com.njcn.rdms.module.system.enums.permission.RoleTypeEnum;
@@ -35,19 +38,8 @@ import java.util.Map;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertMap;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_ADMIN_CODE_ERROR;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_CAN_NOT_DELETE_SYSTEM_TYPE_ROLE;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_CODE_DUPLICATE;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_IS_DISABLE;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_NAME_DUPLICATE;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_NOT_EXISTS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_CREATE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_CREATE_SUCCESS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_DELETE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_DELETE_SUCCESS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_UPDATE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_UPDATE_SUCCESS;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.*;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.*;
@Service
@Slf4j
@@ -59,6 +51,9 @@ public class RoleServiceImpl implements RoleService {
@Resource
private RoleMapper roleMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Override
@Transactional(rollbackFor = Exception.class)
@LogRecord(type = SYSTEM_ROLE_TYPE, subType = SYSTEM_ROLE_CREATE_SUB_TYPE, bizNo = "{{#role.id}}",
@@ -84,6 +79,11 @@ public class RoleServiceImpl implements RoleService {
String effectiveCode = shouldPreserveBuiltInCode(role) ? role.getCode() : updateReqVO.getCode();
validateRoleDuplicate(updateReqVO.getName(), effectiveCode, updateReqVO.getId());
//如果前端想要禁用,则去校验能否被禁用
if (updateReqVO.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) {
VerifyDoDisable(updateReqVO.getId());
}
RoleDO updateObj = BeanUtils.toBean(updateReqVO, RoleDO.class);
if (shouldPreserveBuiltInCode(role)) {
updateObj.setCode(role.getCode());
@@ -118,6 +118,21 @@ public class RoleServiceImpl implements RoleService {
ids.forEach(permissionService::processRoleDeleted);
}
private void VerifyDoDisable(Long id) {
/*
通过角色id去检查是否有用户在使用该角色检查system_user_role表
1.只查deleted = 0的即没被删除的记录
2.无论用户是被禁用还是正常的status = 0 | 1只要有用户使用该角色则不能被禁用
*/
QueryWrapper<UserRoleDO> wrapper = new QueryWrapper<>();
wrapper.eq("deleted", false)
.eq("role_id", id);
Long res = userRoleMapper.selectCount(wrapper);
if (res > 0) {
throw exception(ROLE_DISABLE_NOT_ALLOWED);
}
}
@VisibleForTesting
void validateRoleDuplicate(String name, String code, Long id) {
if (RoleCodeEnum.isBuiltIn(code)) {

View File

@@ -235,4 +235,12 @@ public interface AdminUserService {
*/
boolean isUserAvailable(AdminUserDO user);
/**
* 通过部门ID查询该部门及以下部门的所有用户
* 主要用于下拉框展示用户列表,不分页
*
* @param deptId 部门ID
* @return 用户列表
*/
List<AdminUserDO> getAllUserByDeptId(Long deptId);
}

View File

@@ -39,36 +39,13 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.singleton;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_EMAIL_EXISTS;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_IMPORT_INIT_PASSWORD;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_IMPORT_LIST_IS_EMPTY;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_IS_DISABLE;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_IS_RESIGNED;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_MOBILE_EXISTS;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_NOT_EXISTS;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_PASSWORD_FAILED;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_REGISTER_DISABLED;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_USERNAME_EXISTS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_CREATE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_CREATE_SUCCESS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_DELETE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_DELETE_SUCCESS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_UPDATE_PASSWORD_SUCCESS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_UPDATE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_USER_UPDATE_SUCCESS;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.*;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.*;
/**
* 后台用户 Service 实现类
@@ -97,6 +74,8 @@ public class AdminUserServiceImpl implements AdminUserService {
private OAuth2TokenService oauth2TokenService;
@Resource
private ConfigService configService;
@Resource
private UserManagementRelationService userManagementRelationService;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -218,6 +197,12 @@ public class AdminUserServiceImpl implements AdminUserService {
// 1. 校验用户存在
AdminUserDO user = validateUserExists(id);
// 2. 删除用户及其关联数据
// 2.1 删除前判断管理链路表是否还在用该用户
Boolean res = userManagementRelationService.hasRelation(id);
if (res) {
throw exception(USER_MANAGEMENT_RELATION_EXISTS);
}
// 2.2 确认用户关系表中没使用该用户可继续往下执行
userMapper.deleteById(id);
permissionService.processUserDeleted(id);
oauth2TokenService.removeAccessToken(id, UserTypeEnum.ADMIN.getValue());
@@ -228,6 +213,13 @@ public class AdminUserServiceImpl implements AdminUserService {
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteUserList(List<Long> ids) {
//批量删除前查看是否管理链路表还在使用该用户
for (Long id : ids){
Boolean res = userManagementRelationService.hasRelation(id);
if (res) {
throw exception(USER_MANAGEMENT_RELATION_EXISTS);
}
}
// 1. 批量删除用户
userMapper.deleteByIds(ids);
// 2. 批量删除用户关联数据
@@ -504,6 +496,12 @@ public class AdminUserServiceImpl implements AdminUserService {
&& !isUserResigned(user);
}
@Override
public List<AdminUserDO> getAllUserByDeptId(Long deptId) {
Set<Long> deptCondition = getDeptCondition(deptId);
return getUserListByDeptIds(deptCondition);
}
/**
* 对密码进行加密
*

View File

@@ -0,0 +1,125 @@
package com.njcn.rdms.module.system.service.user;
import cn.hutool.core.collection.CollUtil;
import com.njcn.rdms.framework.common.util.collection.CollectionUtils;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationQueryReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationSaveReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationTreeRespVO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 用户管理链路 Service 接口
*
* @author dklive
*/
public interface UserManagementRelationService {
/**
* 创建用户管理链路
*
* @param createReqVO 关系信息
* @return 关系编号
*/
Long createRelation(UserManagementRelationSaveReqVO createReqVO);
/**
* 更新用户管理链路
*
* @param updateReqVO 关系信息
*/
void updateRelation(UserManagementRelationSaveReqVO updateReqVO);
/**
* 删除用户管理链路
*
* @param id 关系编号
*/
void deleteRelation(Long id);
/**
* 批量删除用户管理链路
*
* @param ids 关系编号数组
*/
void deleteRelationList(List<Long> ids);
/**
* 获得用户管理链路
*
* @param id 关系编号
* @return 用户管理链路
*/
UserManagementRelationDO getRelation(Long id);
/**
* 获得用户管理链路列表
*
* @param ids 关系编号数组
* @return 用户管理链路列表
*/
List<UserManagementRelationDO> getRelationList(Collection<Long> ids);
/**
* 获得用户管理链路列表
*
* @param reqVO 查询条件
* @return 用户管理链路列表
*/
List<UserManagementRelationTreeRespVO> getRelationQuery(UserManagementRelationQueryReqVO reqVO);
/**
* 根据管理者用户ID获得管理链路列表
*
* @param managerUserId 管理者用户ID
* @return 管理链路列表
*/
List<UserManagementRelationDO> getRelationListByManagerUserId(Long managerUserId);
/**
* 根据被管理者用户ID获得管理链路列表
*
* @param subordinateUserId 被管理者用户ID
* @return 管理链路列表
*/
List<UserManagementRelationDO> getRelationListBySubordinateUserId(Long subordinateUserId);
/**
* 获得用户管理链路树形结构
*
* 构建用户上下级关系的树形结构,用于前端树形控件展示
* - 最高领导:没有上级,作为树的根节点
* - 基层员工没有下级children为空列表
*
* @return 用户管理链路树形列表
*/
List<UserManagementRelationTreeRespVO> getRelationTree(UserManagementRelationQueryReqVO reqVO);
/**
* 获得用户管理链路 Map
*
* @param ids 关系编号数组
* @return 用户管理链路 Map
*/
default Map<Long, UserManagementRelationDO> getRelationMap(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return new HashMap<>();
}
return CollectionUtils.convertMap(getRelationList(ids), UserManagementRelationDO::getId);
}
/**
* 通过某个用户的id判断管理链路表中是否有该用户的记录
* 判断原则:
* --管理者ID或被管理者ID有一个字段的值等于用户id且该记录没有被逻辑删除
* --则认为管理链路表中还在使用该用户
*
* @param userId 用户id
* @return true:有该用户的记录false:没有该用户的记录
*/
Boolean hasRelation(Long userId);
}

View File

@@ -0,0 +1,635 @@
package com.njcn.rdms.module.system.service.user;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.annotations.VisibleForTesting;
import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationQueryReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationSaveReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationTreeRespVO;
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import com.njcn.rdms.module.system.dal.mysql.user.AdminUserMapper;
import com.njcn.rdms.module.system.dal.mysql.user.UserManagementRelationMapper;
import jakarta.annotation.Resource;
import lombok.Data;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_MANAGEMENT_RELATION_MANAGER_EXISTS;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.USER_MANAGEMENT_RELATION_NOT_FOUND;
/**
* 用户管理链路 Service 实现类
* 提供用户管理链路的增删改查功能,以及树形结构构建功能
* 树形结构用于前端展示用户的上下级层级关系
* 业务规则:
* 1. 一个用户只能有一个直属上级
* 2. 一个用户可以有多个下属
* 3. 可以将自己设置为被管理者(自己管理自己)
*
* @author dklive
*/
@Service
@Validated
public class UserManagementRelationServiceImpl implements UserManagementRelationService {
@Resource
private UserManagementRelationMapper userManagementRelationMapper;
@Resource
private AdminUserMapper adminUserMapper;
@Resource
private AdminUserService adminUserService;
/**
* 树形结构构建上下文
* 包含构建树形结构所需的所有基础数据
*/
@Data
private static class TreeBuildContext {
private Map<Long, AdminUserDO> userMap;
private Map<Long, List<Long>> managerToSubordinatesMap;
private Map<Long, UserManagementRelationDO> subordinateToRelationMap;
private Set<Long> hasManagerUserIds;
private Set<Long> allUserIds;
}
/**
* 创建用户管理链路
* 业务逻辑:
* 1. 校验被管理者是否已有直属上级(一个用户只能有一个直属上级)
* 2. 插入关系记录
*
* @param createReqVO 创建请求VO
* @return 关系记录主键ID
*/
@Override
public Long createRelation(UserManagementRelationSaveReqVO createReqVO) {
validateRelationForCreateOrUpdate(null, createReqVO.getSubordinateUserId());
UserManagementRelationDO relation = BeanUtils.toBean(createReqVO, UserManagementRelationDO.class);
userManagementRelationMapper.insert(relation);
return relation.getId();
}
/**
* 更新用户管理链路
* 业务逻辑:
* 1. 校验关系记录是否存在
* 2. 校验被管理者是否已有其他直属上级(一个用户只能有一个直属上级)
* 3. 更新关系记录
*
* @param updateReqVO 更新请求VO
*/
@Override
public void updateRelation(UserManagementRelationSaveReqVO updateReqVO) {
validateRelationForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getSubordinateUserId());
UserManagementRelationDO updateObj = BeanUtils.toBean(updateReqVO, UserManagementRelationDO.class);
userManagementRelationMapper.updateById(updateObj);
}
/**
* 删除用户管理链路
* 业务逻辑:
* 1. 校验关系记录是否存在
* 2. 根据主键ID删除关系记录
*
* @param id 关系记录主键ID
*/
@Override
public void deleteRelation(Long id) {
validateRelationExists(id);
userManagementRelationMapper.deleteById(id);
}
/**
* 批量删除用户管理链路
* 业务逻辑:
* 根据主键ID列表批量删除关系记录
*
* @param ids 关系记录主键ID列表
*/
@Override
public void deleteRelationList(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
userManagementRelationMapper.deleteByIds(ids);
}
/**
* 校验关系记录的合法性
* 校验规则:
* 1. 更新时,校验关系记录是否存在
* 2. 被管理者用户只能有一个直属上级(创建时校验,更新时校验是否更换了被管理者)
*
* @param id 关系记录主键ID创建时为null
* @param subordinateUserId 被管理者用户ID
*/
private void validateRelationForCreateOrUpdate(Long id, Long subordinateUserId) {
validateRelationExists(id);
validateSubordinateHasNoOtherManager(id, subordinateUserId);
}
/**
* 校验被管理者是否已有其他直属上级
* 业务规则:一个用户只能有一个直属上级
* 校验逻辑:
* 1. 查询该被管理者用户的所有关系记录
* 2. 如果存在其他关系记录(排除当前记录),则抛出异常
*
* @param excludeId 排除的关系记录主键ID创建时为null更新时为当前记录ID
* @param subordinateUserId 被管理者用户ID
* @throws ServiceException 被管理者已有其他直属上级时抛出异常
*/
private void validateSubordinateHasNoOtherManager(Long excludeId, Long subordinateUserId) {
if (subordinateUserId == null) {
return;
}
List<UserManagementRelationDO> existingRelations =
userManagementRelationMapper.selectListBySubordinateUserId(subordinateUserId);
if (CollUtil.isEmpty(existingRelations)) {
return;
}
if (excludeId == null) {
throw exception(USER_MANAGEMENT_RELATION_MANAGER_EXISTS);
}
boolean hasOtherManager = existingRelations.stream()
.anyMatch(relation -> !relation.getId().equals(excludeId));
if (hasOtherManager) {
throw exception(USER_MANAGEMENT_RELATION_MANAGER_EXISTS);
}
}
/**
* 校验关系记录是否存在
*
* @param id 关系记录主键ID
* @throws ServiceException 关系记录不存在时抛出异常
*/
@VisibleForTesting
void validateRelationExists(Long id) {
if (id == null) {
return;
}
if (userManagementRelationMapper.selectById(id) == null) {
throw exception(USER_MANAGEMENT_RELATION_NOT_FOUND);
}
}
/**
* 根据主键ID获取用户管理链路
*
* @param id 关系记录主键ID
* @return 用户管理链路DO
*/
@Override
public UserManagementRelationDO getRelation(Long id) {
return userManagementRelationMapper.selectOne(
buildValidQuery().eq(UserManagementRelationDO::getId, id)
);
}
/**
* 根据主键ID列表获取用户管理链路列表
*
* @param ids 关系记录主键ID列表
* @return 用户管理链路DO列表
*/
@Override
public List<UserManagementRelationDO> getRelationList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return Collections.emptyList();
}
return userManagementRelationMapper.selectList(
buildValidQuery().in(UserManagementRelationDO::getId, ids)
);
}
/**
* 根据查询条件获取用户管理链路树形结构
* 业务逻辑:
* 1. 构建树形结构上下文(包含所有基础数据)
* 2. 根据查询条件确定目标用户IDsubordinateUserId优先于managerUserId
* 3. 如果有目标用户,只构建包含该用户的分支(上级链 + 下级树)
* 4. 如果没有目标用户,返回完整的树形结构
* <p>
* 查询参数优先级:
* - 当subordinateUserId和managerUserId同时存在时优先使用subordinateUserId
* - 正常业务场景下,两个参数只会传递其中一个
* <p>
* 边界情况处理:
* - 两个参数都为null返回完整的树形结构等同于getRelationTree
* - subordinateUserId不为null构建包含该用户的上级链和下级树不包含同级节点
* - managerUserId不为null构建以该用户为根节点的完整下级树
*
* @param reqVO 查询条件VO包含subordinateUserId或managerUserId
* @return 用户管理链路树形结构列表
*/
@Override
public List<UserManagementRelationTreeRespVO> getRelationQuery(UserManagementRelationQueryReqVO reqVO) {
TreeBuildContext context = buildTreeContext(reqVO);
if (context == null) {
return Collections.emptyList();
}
Long targetUserId = determineTargetUserId(reqVO, context);
if (targetUserId == null) {
return buildFullTree(context);
}
return buildTargetBranchTree(targetUserId, context);
}
/**
* 根据管理者用户ID获取其下属关系列表
*
* @param managerUserId 管理者用户ID
* @return 用户管理链路DO列表
*/
@Override
public List<UserManagementRelationDO> getRelationListByManagerUserId(Long managerUserId) {
return userManagementRelationMapper.selectListByManagerUserId(managerUserId);
}
/**
* 根据被管理者用户ID获取其上级关系列表
* 注意:由于一个用户只能有一个直属上级,此方法最多返回一条记录
*
* @param subordinateUserId 被管理者用户ID
* @return 用户管理链路DO列表
*/
@Override
public List<UserManagementRelationDO> getRelationListBySubordinateUserId(Long subordinateUserId) {
return userManagementRelationMapper.selectListBySubordinateUserId(subordinateUserId);
}
/**
* 获取用户管理链路树形结构
* 业务逻辑:
* 1. 构建树形结构上下文(包含所有基础数据)
* 2. 找出所有根节点(没有上级的用户,即最高领导;也包括自己管理自己的用户)
* 3. 从根节点开始递归构建完整的树形结构
* <p>
* 边界情况处理:
* - 根节点还没有设置直属上级的用户managerUserId设置为自己的userIdmanagerNickname为自己的昵称
* - 自己管理自己的用户managerUserId等于userIdmanagerNickname为自己的昵称
* - 基层员工没有下级children为空列表
* - 空数据:返回空列表
*
* @return 用户管理链路树形结构列表
*/
@Override
public List<UserManagementRelationTreeRespVO> getRelationTree(UserManagementRelationQueryReqVO reqVO) {
TreeBuildContext context = buildTreeContext(reqVO);
if (context == null) {
return Collections.emptyList();
}
return buildFullTree(context);
}
/**
* 通过某个用户的id判断管理链路表中是否有该用户的记录
* 判断原则:
* --管理者ID或被管理者ID有一个字段的值等于用户id且该记录没有被逻辑删除
* --则认为管理链路表中还在使用该用户
*
* @param userId 用户id
* @return true:有该用户的记录false:没有该用户的记录
*/
@Override
public Boolean hasRelation(Long userId) {
if (userId == null) {
return false;
}
LambdaQueryWrapperX<UserManagementRelationDO> wrapper = new LambdaQueryWrapperX<>();
wrapper.eq(UserManagementRelationDO::getManagerUserId, userId)
.or()
.eq(UserManagementRelationDO::getSubordinateUserId, userId);
return userManagementRelationMapper.selectCount(wrapper) > 0;
}
/**
* 构建树形结构上下文
* 查询并组装构建树形结构所需的所有基础数据
*
* @param reqVO 查询条件VO包含fromUserIndex和deptId
* @return 树形结构上下文如果没有数据则返回null
*/
private TreeBuildContext buildTreeContext(UserManagementRelationQueryReqVO reqVO) {
List<UserManagementRelationDO> allRelations = userManagementRelationMapper.selectList(new UserManagementRelationQueryReqVO());
if (CollUtil.isEmpty(allRelations)) {
return null;
}
if (Boolean.TRUE.equals(reqVO.getFromUserIndex()) && reqVO.getDeptId() != null && reqVO.getDeptId() != 100L) {
List<AdminUserDO> deptUsers = adminUserService.getAllUserByDeptId(reqVO.getDeptId());
Set<Long> deptUserIds = deptUsers.stream().map(AdminUserDO::getId).collect(Collectors.toSet());
allRelations = allRelations.stream()
.filter(relation -> deptUserIds.contains(relation.getManagerUserId())
&& deptUserIds.contains(relation.getSubordinateUserId()))
.collect(Collectors.toList());
if (CollUtil.isEmpty(allRelations)) {
return null;
}
}
Set<Long> allUserIds = new HashSet<>();
for (UserManagementRelationDO relation : allRelations) {
allUserIds.add(relation.getManagerUserId());
allUserIds.add(relation.getSubordinateUserId());
}
List<AdminUserDO> users = adminUserMapper.selectByIds(allUserIds);
Map<Long, AdminUserDO> userMap = users.stream()
.collect(Collectors.toMap(AdminUserDO::getId, user -> user));
Map<Long, List<Long>> managerToSubordinatesMap = allRelations.stream()
.collect(Collectors.groupingBy(
UserManagementRelationDO::getManagerUserId,
Collectors.mapping(UserManagementRelationDO::getSubordinateUserId, Collectors.toList())
));
Map<Long, UserManagementRelationDO> subordinateToRelationMap = allRelations.stream()
.collect(Collectors.toMap(
UserManagementRelationDO::getSubordinateUserId,
relation -> relation,
(existing, replacement) -> existing
));
Set<Long> hasManagerUserIds = allRelations.stream()
.filter(relation -> !relation.getManagerUserId().equals(relation.getSubordinateUserId()))
.map(UserManagementRelationDO::getSubordinateUserId)
.collect(Collectors.toSet());
TreeBuildContext context = new TreeBuildContext();
context.setUserMap(userMap);
context.setManagerToSubordinatesMap(managerToSubordinatesMap);
context.setSubordinateToRelationMap(subordinateToRelationMap);
context.setHasManagerUserIds(hasManagerUserIds);
context.setAllUserIds(allUserIds);
return context;
}
/**
* 构建完整的树形结构
* 从所有根节点开始构建完整的树形结构
*
* @param context 树形结构上下文
* @return 完整的树形结构列表
*/
private List<UserManagementRelationTreeRespVO> buildFullTree(TreeBuildContext context) {
List<Long> rootUserIds = context.getAllUserIds().stream()
.filter(userId -> !context.getHasManagerUserIds().contains(userId))
.toList();
List<UserManagementRelationTreeRespVO> treeList = new ArrayList<>();
for (Long rootUserId : rootUserIds) {
UserManagementRelationTreeRespVO rootNode = buildRootNode(rootUserId, context);
if (rootNode != null) {
treeList.add(rootNode);
}
}
return treeList;
}
/**
* 构建目标用户相关的分支树形结构
* 只包含目标用户的上级链和下级树,不包含同级节点
* <p>
* 构建逻辑:
* 1. 从目标用户向上追溯到根节点,记录上级链
* 2. 从根节点开始,只沿着包含目标用户的分支向下构建
* 3. 构建目标用户的所有下级
*
* @param targetUserId 目标用户ID
* @param context 树形结构上下文
* @return 目标用户相关的分支树形结构列表
*/
private List<UserManagementRelationTreeRespVO> buildTargetBranchTree(Long targetUserId, TreeBuildContext context) {
LinkedList<Long> pathToRoot = findPathToRoot(targetUserId, context);
if (pathToRoot.isEmpty()) {
return Collections.emptyList();
}
Long rootUserId = pathToRoot.getFirst();
UserManagementRelationTreeRespVO rootNode = buildRootNode(rootUserId, context);
if (rootNode == null) {
return Collections.emptyList();
}
if (pathToRoot.size() > 1) {
buildBranchPath(rootNode, pathToRoot, context);
}
return Collections.singletonList(rootNode);
}
/**
* 从目标用户向上追溯到根节点,记录路径
*
* @param targetUserId 目标用户ID
* @param context 树形结构上下文
* @return 从根节点到目标用户的路径(包含根节点和目标用户)
*/
private LinkedList<Long> findPathToRoot(Long targetUserId, TreeBuildContext context) {
LinkedList<Long> path = new LinkedList<>();
Set<Long> visited = new HashSet<>();
Long currentUserId = targetUserId;
while (currentUserId != null && !visited.contains(currentUserId)) {
visited.add(currentUserId);
path.addFirst(currentUserId);
if (!context.getHasManagerUserIds().contains(currentUserId)) {
break;
}
UserManagementRelationDO relation = context.getSubordinateToRelationMap().get(currentUserId);
if (relation == null) {
break;
}
Long managerUserId = relation.getManagerUserId();
if (managerUserId.equals(currentUserId)) {
break;
}
currentUserId = managerUserId;
}
return path;
}
/**
* 沿着指定路径构建分支
* 从根节点的下一层开始,只构建路径中包含的用户节点
*
* @param parentNode 父节点
* @param pathToRoot 从根节点到目标用户的路径
* @param context 树形结构上下文
*/
private void buildBranchPath(UserManagementRelationTreeRespVO parentNode, LinkedList<Long> pathToRoot, TreeBuildContext context) {
if (pathToRoot.size() <= 1) {
return;
}
Long parentUserId = parentNode.getUserId();
List<Long> subordinateIds = context.getManagerToSubordinatesMap().get(parentUserId);
if (CollUtil.isEmpty(subordinateIds)) {
return;
}
Long nextUserIdInPath = pathToRoot.get(1);
for (Long subordinateId : subordinateIds) {
if (subordinateId.equals(parentUserId)) {
continue;
}
UserManagementRelationDO childRelation = context.getSubordinateToRelationMap().get(subordinateId);
Long childRelationId = childRelation != null ? childRelation.getId() : null;
if (subordinateId.equals(nextUserIdInPath)) {
UserManagementRelationTreeRespVO childNode = buildTreeNode(childRelationId, subordinateId, parentUserId, context);
if (childNode != null) {
parentNode.setChildren(Collections.singletonList(childNode));
if (pathToRoot.size() > 2) {
LinkedList<Long> remainingPath = new LinkedList<>(pathToRoot.subList(1, pathToRoot.size()));
buildBranchPath(childNode, remainingPath, context);
}
}
break;
}
}
}
/**
* 构建根节点
*
* @param rootUserId 根节点用户ID
* @param context 树形结构上下文
* @return 根节点
*/
private UserManagementRelationTreeRespVO buildRootNode(Long rootUserId, TreeBuildContext context) {
UserManagementRelationDO rootRelation = context.getSubordinateToRelationMap().get(rootUserId);
Long rootRelationId = rootRelation != null ? rootRelation.getId() : null;
Long rootManagerUserId;
if (rootRelation == null) {
rootManagerUserId = rootUserId;
} else if (rootRelation.getManagerUserId().equals(rootRelation.getSubordinateUserId())) {
rootManagerUserId = rootUserId;
} else {
rootManagerUserId = rootRelation.getManagerUserId();
}
return buildTreeNode(rootRelationId, rootUserId, rootManagerUserId, context);
}
/**
* 确定目标用户ID
* 根据查询参数优先级确定目标用户:
* 1. 如果subordinateUserId不为null返回subordinateUserId
* 2. 如果managerUserId不为null返回managerUserId
* 3. 否则返回null表示查询所有
*
* @param reqVO 查询条件VO
* @param context 树形结构上下文
* @return 目标用户ID如果没有指定则返回null
*/
private Long determineTargetUserId(UserManagementRelationQueryReqVO reqVO, TreeBuildContext context) {
if (reqVO == null) {
return null;
}
if (reqVO.getSubordinateUserId() != null && context.getAllUserIds().contains(reqVO.getSubordinateUserId())) {
return reqVO.getSubordinateUserId();
}
if (reqVO.getManagerUserId() != null && context.getAllUserIds().contains(reqVO.getManagerUserId())) {
return reqVO.getManagerUserId();
}
return null;
}
/**
* 构建树形节点
* 构建逻辑:
* 1. 根据用户ID获取用户信息
* 2. 设置节点的基本信息关系记录ID、用户ID、用户昵称
* 3. 设置上级信息上级用户ID、上级用户昵称
* 4. 递归构建所有下级节点列表
*
* @param relationId 关系记录主键ID根节点且非自己管理自己时为null
* @param userId 当前用户ID
* @param managerUserId 上级用户ID根节点时为自己的userId
* @param context 树形结构上下文
* @return 树形节点
*/
private UserManagementRelationTreeRespVO buildTreeNode(Long relationId, Long userId, Long managerUserId,
TreeBuildContext context) {
AdminUserDO user = context.getUserMap().get(userId);
if (user == null) {
return null;
}
UserManagementRelationTreeRespVO node = new UserManagementRelationTreeRespVO();
node.setId(relationId);
node.setUserId(userId);
node.setUserNickname(user.getNickname());
node.setManagerUserId(managerUserId);
if (managerUserId != null) {
AdminUserDO managerUser = context.getUserMap().get(managerUserId);
if (managerUser != null) {
node.setManagerNickname(managerUser.getNickname());
}
}
List<Long> subordinateIds = context.getManagerToSubordinatesMap().get(userId);
if (CollUtil.isNotEmpty(subordinateIds)) {
List<UserManagementRelationTreeRespVO> children = new ArrayList<>();
for (Long subordinateId : subordinateIds) {
if (subordinateId.equals(userId)) {
continue;
}
UserManagementRelationDO childRelation = context.getSubordinateToRelationMap().get(subordinateId);
Long childRelationId = childRelation != null ? childRelation.getId() : null;
UserManagementRelationTreeRespVO childNode = buildTreeNode(childRelationId, subordinateId, userId, context);
if (childNode != null) {
children.add(childNode);
}
}
node.setChildren(children);
} else {
node.setChildren(Collections.emptyList());
}
return node;
}
/**
* 统一时间有效性过滤条件:
* <p>
* 1.如果这两个字段都是null那就直接查出来
* 2.如果effective_util是null、effective_from不是null那就只需要当前时间>=effective_from就行
* 3.如果effective_from是null、effective_util不是null那就只需要当前时间<=effective_util就行
* <p>
* 如果上面三个条件都不满足,那就判断为此条关系已经失效,不查出来
*/
private LambdaQueryWrapper<UserManagementRelationDO> buildValidQuery() {
LocalDateTime now = LocalDateTime.now();
return new LambdaQueryWrapper<UserManagementRelationDO>()
// (from IS NULL OR from <= now)
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveFrom)
.or().le(UserManagementRelationDO::getEffectiveFrom, now))
// (until IS NULL OR until >= now)
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveUntil)
.or().ge(UserManagementRelationDO::getEffectiveUntil, now));
// BaseMapperX 通常已自动处理 deleted 字段,无需额外加
}
}

View File

@@ -7,12 +7,12 @@ spring:
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 数据库相关配置 ####################

View File

@@ -6,12 +6,12 @@ spring:
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 数据库相关配置 ####################

View File

@@ -32,6 +32,10 @@
redis:
repositories:
enabled: false # 项目未使用到 Spring Data Redis 的 Repository所以直接禁用保证启动速度
# 热部署配置
devtools:
restart:
enabled: true
server:
port: 48081
@@ -40,7 +44,6 @@ logging:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
--- #################### 接口文档配置 ####################
springdoc:
api-docs:
enabled: true # 1. 是否开启 Swagger 接文档的元数据