fix(产品需求): 解决测试后存在的一些问题。
This commit is contained in:
@@ -16,19 +16,19 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "父需求ID(0表示顶级需求)", example = "0")
|
||||
@Schema(description = "父需求ID,0 表示顶级需求", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "所属模块ID", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", example = "0")
|
||||
@Schema(description = "是否需要评审,0不需要,1需要", example = "0")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@@ -37,13 +37,13 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "需求分类名称", example = "功能需求")
|
||||
private String categoryName;
|
||||
|
||||
@Schema(description = "来源类型(manual:手工新增, work_order:工单流转)", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual")
|
||||
@Schema(description = "需求来源类型,manual 表示手工新增,work_order 表示工单流转", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "来源业务ID", example = "1024")
|
||||
private Long sourceBizId;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@Schema(description = "优先级,0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "优先级名称", example = "中")
|
||||
@@ -58,13 +58,16 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "提出人用户姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
@Schema(description = "所需工时", example = "8")
|
||||
private Double workHours;
|
||||
|
||||
@Schema(description = "当前处理人用户ID", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
@@ -76,9 +79,6 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "实现项目名称", example = "NPQS-10086")
|
||||
private String implementProjectName;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@@ -88,10 +88,10 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "子需求列表(树形结构)")
|
||||
@Schema(description = "子需求列表,树形结构")
|
||||
private List<ProductRequirementRespVO> children;
|
||||
|
||||
@Schema(description = "是否为终态(已拒绝、已取消、已关闭)", example = "false")
|
||||
@Schema(description = "是否为终态,已拒绝、已取消、已关闭都算终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求保存 Request VO
|
||||
*/
|
||||
@@ -22,45 +20,51 @@ public class ProductRequirementSaveReqVO {
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
@Schema(description = "所属模块ID,为空时归入全部需求模块", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@Schema(description = "是否需要评审,0不需要,1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
@NotBlank(message = "需求标题不能为空")
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
@Size(max = 200, message = "需求标题长度不能超过 200 个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
@Size(max = 64, message = "需求分类长度不能超过 64 个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@Schema(description = "优先级,0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
@Schema(description = "提出人姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "所需工时", example = "8")
|
||||
@NotNull(message = "所需工时不能为空")
|
||||
private Double workHours;
|
||||
|
||||
@Schema(description = "当前处理人用户ID", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
private String currentHandlerUserNickname;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
@Schema(description = "排序值,越小越靠前", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求拆分 Request VO
|
||||
*/
|
||||
@@ -23,45 +21,51 @@ public class ProductRequirementSplitReqVO {
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
@Schema(description = "所属模块ID,为空时归入全部需求模块", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@Schema(description = "是否需要评审,0不需要,1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
@NotBlank(message = "需求标题不能为空")
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
@Size(max = 200, message = "需求标题长度不能超过 200 个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
@Size(max = 64, message = "需求分类长度不能超过 64 个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@Schema(description = "优先级,0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
@Schema(description = "提出人姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "所需工时", example = "8")
|
||||
@NotNull(message = "所需工时不能为空")
|
||||
private Double workHours;
|
||||
|
||||
@Schema(description = "当前处理人用户ID", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
private String currentHandlerUserNickname;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
@Schema(description = "排序值,越小越靠前", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求编辑 Request VO
|
||||
*/
|
||||
@@ -23,45 +21,51 @@ public class ProductRequirementUpdateReqVO {
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
@Schema(description = "所属模块ID,为空时归入全部需求模块", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@Schema(description = "是否需要评审,0不需要,1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
@NotBlank(message = "需求标题不能为空")
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
@Size(max = 200, message = "需求标题长度不能超过 200 个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
@Size(max = 64, message = "需求分类长度不能超过 64 个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@Schema(description = "优先级,0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
@Schema(description = "提出人姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "所需工时", example = "8")
|
||||
@NotNull(message = "所需工时不能为空")
|
||||
private Double workHours;
|
||||
|
||||
@Schema(description = "当前处理人用户ID", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
private String currentHandlerUserNickname;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
@Schema(description = "排序值,越小越靠前", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 产品需求主表
|
||||
*/
|
||||
@@ -22,11 +20,11 @@ public class ProductRequirementDO extends BaseDO {
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 父需求ID(0表示顶级需求)
|
||||
* 父需求ID,0 表示顶级需求
|
||||
*/
|
||||
private Long parentId;
|
||||
/**
|
||||
* 所属模块ID(0表示全部需求)
|
||||
* 所属模块ID,空表示全部需求
|
||||
*/
|
||||
private Long moduleId;
|
||||
/**
|
||||
@@ -34,7 +32,7 @@ public class ProductRequirementDO extends BaseDO {
|
||||
*/
|
||||
private Long productId;
|
||||
/**
|
||||
* 是否需要评审(0不需要;1需要)
|
||||
* 是否需要评审,0不需要,1需要
|
||||
*/
|
||||
private Integer reviewRequired;
|
||||
/**
|
||||
@@ -42,7 +40,7 @@ public class ProductRequirementDO extends BaseDO {
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 需求描述(富文本)
|
||||
* 需求描述,支持富文本
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
@@ -50,7 +48,7 @@ public class ProductRequirementDO extends BaseDO {
|
||||
*/
|
||||
private String category;
|
||||
/**
|
||||
* 来源类型(manual:手工新增, work_order:工单流转)
|
||||
* 来源类型,manual 表示手工新增,work_order 表示工单流转
|
||||
*/
|
||||
private String sourceType;
|
||||
/**
|
||||
@@ -58,7 +56,7 @@ public class ProductRequirementDO extends BaseDO {
|
||||
*/
|
||||
private Long sourceBizId;
|
||||
/**
|
||||
* 优先级(0低 1中 2高 3紧急)
|
||||
* 优先级,0低、1中、2高、3紧急
|
||||
*/
|
||||
private Integer priority;
|
||||
/**
|
||||
@@ -74,9 +72,13 @@ public class ProductRequirementDO extends BaseDO {
|
||||
*/
|
||||
private Long proposerId;
|
||||
/**
|
||||
* 提出人用户姓名快照
|
||||
* 提出人姓名快照
|
||||
*/
|
||||
private String proposerNickname;
|
||||
/**
|
||||
* 所需工时
|
||||
*/
|
||||
private Double workHours;
|
||||
/**
|
||||
* 当前处理人用户ID
|
||||
*/
|
||||
@@ -86,15 +88,11 @@ public class ProductRequirementDO extends BaseDO {
|
||||
*/
|
||||
private String currentHandlerUserNickname;
|
||||
/**
|
||||
* 默认实现项目ID(分流后填写)
|
||||
* 默认实现项目ID,分流后可回填
|
||||
*/
|
||||
private Long implementProjectId;
|
||||
/**
|
||||
* 预期完成时间
|
||||
*/
|
||||
private LocalDateTime completionDate;
|
||||
/**
|
||||
* 排序值(越小越靠前)
|
||||
* 排序值,越小越靠前
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
|
||||
@@ -25,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
@@ -56,6 +58,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
private static final List<String> CHILD_ALLOW_CLOSE_STATUSES = List.of(STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED);
|
||||
// 允许删除的状态集合(实施中之前的状态)
|
||||
private static final List<String> ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH);
|
||||
// 父需求取消时,子需求允许的状态集合(仅已拒绝和已取消)
|
||||
private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(STATUS_REJECTED, STATUS_CANCELLED);
|
||||
|
||||
// 权限常量
|
||||
private static final String PRODUCT_CREATE_PERMISSION = "project:product:create";
|
||||
@@ -113,14 +117,17 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
|
||||
requirement.setStatusCode(initialStatus);
|
||||
requirement.setProposerId(createReqVO.getProposerId());
|
||||
requirement.setProposerNickname(normalizeNullableText(createReqVO.getProposerNickname()));
|
||||
requirement.setWorkHours(createReqVO.getWorkHours());
|
||||
requirement.setCurrentHandlerUserId(createReqVO.getCurrentHandlerUserId());
|
||||
requirement.setCurrentHandlerUserNickname(normalizeNullableText(createReqVO.getCurrentHandlerUserNickname()));
|
||||
requirement.setImplementProjectId(createReqVO.getImplementProjectId());
|
||||
requirement.setCompletionDate(createReqVO.getCompletionDate());
|
||||
requirement.setSort(createReqVO.getSort() != null ? createReqVO.getSort() : 0);
|
||||
requirementMapper.insert(requirement);
|
||||
|
||||
// 写入业务审计日志
|
||||
writeBizAuditLog(requirement, ACTION_CREATE, null, initialStatus, null, null);
|
||||
writeBizAuditLog(requirement, ACTION_CREATE, null, initialStatus,
|
||||
buildRequirementFieldChanges(null, requirement), null);
|
||||
return requirement.getId();
|
||||
}
|
||||
|
||||
@@ -137,6 +144,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
// 校验模块是否属于当前产品
|
||||
validateModuleBelongsToProduct(moduleId, updateReqVO.getProductId());
|
||||
|
||||
ProductRequirementDO before = cloneRequirement(requirement);
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
requirement.setModuleId(moduleId);
|
||||
requirement.setReviewRequired(updateReqVO.getReviewRequired());
|
||||
@@ -145,13 +153,16 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
requirement.setCategory(updateReqVO.getCategory());
|
||||
requirement.setPriority(updateReqVO.getPriority());
|
||||
requirement.setProposerId(updateReqVO.getProposerId());
|
||||
requirement.setProposerNickname(normalizeNullableText(updateReqVO.getProposerNickname()));
|
||||
requirement.setWorkHours(updateReqVO.getWorkHours());
|
||||
requirement.setCurrentHandlerUserId(updateReqVO.getCurrentHandlerUserId());
|
||||
requirement.setCurrentHandlerUserNickname(normalizeNullableText(updateReqVO.getCurrentHandlerUserNickname()));
|
||||
requirement.setImplementProjectId(updateReqVO.getImplementProjectId());
|
||||
requirement.setCompletionDate(updateReqVO.getCompletionDate());
|
||||
requirement.setSort(updateReqVO.getSort() != null ? updateReqVO.getSort() : 0);
|
||||
requirementMapper.updateById(requirement);
|
||||
|
||||
writeBizAuditLog(requirement, ACTION_UPDATE, fromStatus, requirement.getStatusCode(), null, null);
|
||||
writeBizAuditLog(requirement, ACTION_UPDATE, fromStatus, requirement.getStatusCode(),
|
||||
buildRequirementFieldChanges(before, requirement), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -370,6 +381,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
String actionCode = reqVO.getActionCode().trim();
|
||||
Long implementProjectId = reqVO.getImplementProjectId();
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
ProductRequirementDO before = cloneRequirement(requirement);
|
||||
|
||||
// 校验状态流转是否合法
|
||||
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
||||
@@ -383,6 +395,10 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
if ("accept".equals(actionCode) || "close".equals(actionCode)) {
|
||||
validateAllChildrenAllowCloseOrAccept(reqVO.getId());
|
||||
}
|
||||
// cancel动作时,如果是父需求则校验所有子需求是否处于已拒绝或已取消
|
||||
if ("cancel".equals(actionCode) && Objects.equals(requirement.getParentId(), 0L)) {
|
||||
validateParentCancelAllowed(reqVO.getId());
|
||||
}
|
||||
// close动作时,递归关闭所有已验收的子需求(包括子子需求)
|
||||
if ("close".equals(actionCode)) {
|
||||
closeAllAcceptedChildren(reqVO.getId(), reason);
|
||||
@@ -394,11 +410,15 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
}
|
||||
requirement.setStatusCode(toStatus);
|
||||
requirement.setLastStatusReason(reason);
|
||||
if (implementProjectId != null) {
|
||||
requirement.setImplementProjectId(implementProjectId);
|
||||
}
|
||||
|
||||
// 写入状态变更日志
|
||||
writeRequirementStatusLog(requirement, actionCode, fromStatus, toStatus, reason);
|
||||
// 写入业务审计日志
|
||||
writeBizAuditLog(requirement, actionCode, fromStatus, toStatus, null, reason);
|
||||
writeBizAuditLog(requirement, actionCode, fromStatus, toStatus,
|
||||
buildRequirementFieldChanges(before, requirement), reason);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,6 +437,23 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验父需求取消时,所有子需求(包括子子需求)是否处于允许取消的状态
|
||||
* 只有子需求全部为已拒绝或已取消时,父需求才允许取消
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateParentCancelAllowed(Long requirementId) {
|
||||
List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
||||
// 排除自身,只校验子需求
|
||||
for (ProductRequirementDO req : allChildren) {
|
||||
if (!Objects.equals(req.getId(), requirementId)) {
|
||||
if (!CHILD_ALLOW_CANCEL_STATUSES.contains(req.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CANCEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
@@ -442,7 +479,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
|
||||
writeBizAuditLog(requirement, ACTION_DELETE, fromStatus, null, null, null);
|
||||
writeBizAuditLog(requirement, ACTION_DELETE, fromStatus, null,
|
||||
buildRequirementFieldChanges(requirement, null), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -471,27 +509,32 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
String initialStatus = Objects.equals(reqVO.getReviewRequired(), 1)
|
||||
? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
|
||||
childRequirement.setStatusCode(initialStatus);
|
||||
childRequirement.setProposerId(parentRequirement.getProposerId()); // 继承父需求提出人
|
||||
childRequirement.setProposerId(reqVO.getProposerId());
|
||||
childRequirement.setProposerNickname(normalizeNullableText(reqVO.getProposerNickname()));
|
||||
childRequirement.setWorkHours(reqVO.getWorkHours());
|
||||
childRequirement.setCurrentHandlerUserId(reqVO.getCurrentHandlerUserId());
|
||||
childRequirement.setCurrentHandlerUserNickname(normalizeNullableText(reqVO.getCurrentHandlerUserNickname()));
|
||||
childRequirement.setImplementProjectId(reqVO.getImplementProjectId());
|
||||
childRequirement.setCompletionDate(reqVO.getCompletionDate());
|
||||
childRequirement.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
|
||||
requirementMapper.insert(childRequirement);
|
||||
|
||||
// 父需求状态从待分流变为实施中
|
||||
if (STATUS_PENDING_DISPATCH.equals(parentRequirement.getStatusCode())) {
|
||||
ProductRequirementDO before = cloneRequirement(parentRequirement);
|
||||
String fromStatus = parentRequirement.getStatusCode();
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||
parentRequirement.getId(), fromStatus, STATUS_IMPLEMENTING, null);
|
||||
if (updateCount == 1) {
|
||||
parentRequirement.setStatusCode(STATUS_IMPLEMENTING);
|
||||
writeRequirementStatusLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null);
|
||||
writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null, null);
|
||||
writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING,
|
||||
buildRequirementFieldChanges(before, parentRequirement), null);
|
||||
}
|
||||
}
|
||||
|
||||
// 写入子需求的业务审计日志
|
||||
writeBizAuditLog(childRequirement, ACTION_CREATE, null, initialStatus, null, null);
|
||||
writeBizAuditLog(childRequirement, ACTION_CREATE, null, initialStatus,
|
||||
buildRequirementFieldChanges(null, childRequirement), null);
|
||||
return childRequirement.getId();
|
||||
}
|
||||
|
||||
@@ -503,6 +546,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
String reason = reqVO.getReason().trim();
|
||||
ProductRequirementDO before = cloneRequirement(requirement);
|
||||
|
||||
// 校验当前状态是否为已验收(只有已验收才能关闭)
|
||||
if (!STATUS_ACCEPTED.equals(fromStatus)) {
|
||||
@@ -525,7 +569,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
requirement.setLastStatusReason(reason);
|
||||
|
||||
writeRequirementStatusLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, reason);
|
||||
writeBizAuditLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, null, reason);
|
||||
writeBizAuditLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED,
|
||||
buildRequirementFieldChanges(before, requirement), reason);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -538,11 +583,15 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
closeAllAcceptedChildren(child.getId(), reason);
|
||||
// 如果子需求已验收,则关闭
|
||||
if (STATUS_ACCEPTED.equals(child.getStatusCode())) {
|
||||
ProductRequirementDO before = cloneRequirement(child);
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||
child.getId(), STATUS_ACCEPTED, STATUS_CLOSED, reason);
|
||||
if (updateCount == 1) {
|
||||
child.setStatusCode(STATUS_CLOSED);
|
||||
child.setLastStatusReason(reason);
|
||||
writeRequirementStatusLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, reason);
|
||||
writeBizAuditLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, null, reason);
|
||||
writeBizAuditLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED,
|
||||
buildRequirementFieldChanges(before, child), reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -894,6 +943,98 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆需求快照,避免更新前后的对象引用互相污染。
|
||||
*/
|
||||
private ProductRequirementDO cloneRequirement(ProductRequirementDO source) {
|
||||
ProductRequirementDO target = new ProductRequirementDO();
|
||||
target.setId(source.getId());
|
||||
target.setParentId(source.getParentId());
|
||||
target.setModuleId(source.getModuleId());
|
||||
target.setProductId(source.getProductId());
|
||||
target.setReviewRequired(source.getReviewRequired());
|
||||
target.setTitle(source.getTitle());
|
||||
target.setDescription(source.getDescription());
|
||||
target.setCategory(source.getCategory());
|
||||
target.setSourceType(source.getSourceType());
|
||||
target.setSourceBizId(source.getSourceBizId());
|
||||
target.setPriority(source.getPriority());
|
||||
target.setStatusCode(source.getStatusCode());
|
||||
target.setLastStatusReason(source.getLastStatusReason());
|
||||
target.setProposerId(source.getProposerId());
|
||||
target.setProposerNickname(source.getProposerNickname());
|
||||
target.setWorkHours(source.getWorkHours());
|
||||
target.setCurrentHandlerUserId(source.getCurrentHandlerUserId());
|
||||
target.setCurrentHandlerUserNickname(source.getCurrentHandlerUserNickname());
|
||||
target.setImplementProjectId(source.getImplementProjectId());
|
||||
target.setSort(source.getSort());
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建需求字段变更内容,供业务审计日志落库。
|
||||
*/
|
||||
private String buildRequirementFieldChanges(ProductRequirementDO before, ProductRequirementDO after) {
|
||||
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||
appendFieldChange(fieldChanges, "parentId", valueOf(before, ProductRequirementDO::getParentId),
|
||||
valueOf(after, ProductRequirementDO::getParentId));
|
||||
appendFieldChange(fieldChanges, "moduleId", valueOf(before, ProductRequirementDO::getModuleId),
|
||||
valueOf(after, ProductRequirementDO::getModuleId));
|
||||
appendFieldChange(fieldChanges, "productId", valueOf(before, ProductRequirementDO::getProductId),
|
||||
valueOf(after, ProductRequirementDO::getProductId));
|
||||
appendFieldChange(fieldChanges, "reviewRequired", valueOf(before, ProductRequirementDO::getReviewRequired),
|
||||
valueOf(after, ProductRequirementDO::getReviewRequired));
|
||||
appendFieldChange(fieldChanges, "title", valueOf(before, ProductRequirementDO::getTitle),
|
||||
valueOf(after, ProductRequirementDO::getTitle));
|
||||
appendFieldChange(fieldChanges, "description", valueOf(before, ProductRequirementDO::getDescription),
|
||||
valueOf(after, ProductRequirementDO::getDescription));
|
||||
appendFieldChange(fieldChanges, "category", valueOf(before, ProductRequirementDO::getCategory),
|
||||
valueOf(after, ProductRequirementDO::getCategory));
|
||||
appendFieldChange(fieldChanges, "sourceType", valueOf(before, ProductRequirementDO::getSourceType),
|
||||
valueOf(after, ProductRequirementDO::getSourceType));
|
||||
appendFieldChange(fieldChanges, "sourceBizId", valueOf(before, ProductRequirementDO::getSourceBizId),
|
||||
valueOf(after, ProductRequirementDO::getSourceBizId));
|
||||
appendFieldChange(fieldChanges, "priority", valueOf(before, ProductRequirementDO::getPriority),
|
||||
valueOf(after, ProductRequirementDO::getPriority));
|
||||
appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProductRequirementDO::getStatusCode),
|
||||
valueOf(after, ProductRequirementDO::getStatusCode));
|
||||
appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, ProductRequirementDO::getLastStatusReason),
|
||||
valueOf(after, ProductRequirementDO::getLastStatusReason));
|
||||
appendFieldChange(fieldChanges, "proposerId", valueOf(before, ProductRequirementDO::getProposerId),
|
||||
valueOf(after, ProductRequirementDO::getProposerId));
|
||||
appendFieldChange(fieldChanges, "proposerNickname", valueOf(before, ProductRequirementDO::getProposerNickname),
|
||||
valueOf(after, ProductRequirementDO::getProposerNickname));
|
||||
appendFieldChange(fieldChanges, "workHours", valueOf(before, ProductRequirementDO::getWorkHours),
|
||||
valueOf(after, ProductRequirementDO::getWorkHours));
|
||||
appendFieldChange(fieldChanges, "currentHandlerUserId", valueOf(before, ProductRequirementDO::getCurrentHandlerUserId),
|
||||
valueOf(after, ProductRequirementDO::getCurrentHandlerUserId));
|
||||
appendFieldChange(fieldChanges, "currentHandlerUserNickname",
|
||||
valueOf(before, ProductRequirementDO::getCurrentHandlerUserNickname),
|
||||
valueOf(after, ProductRequirementDO::getCurrentHandlerUserNickname));
|
||||
appendFieldChange(fieldChanges, "implementProjectId", valueOf(before, ProductRequirementDO::getImplementProjectId),
|
||||
valueOf(after, ProductRequirementDO::getImplementProjectId));
|
||||
appendFieldChange(fieldChanges, "sort", valueOf(before, ProductRequirementDO::getSort),
|
||||
valueOf(after, ProductRequirementDO::getSort));
|
||||
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
|
||||
}
|
||||
|
||||
private <T> T valueOf(ProductRequirementDO requirement, Function<ProductRequirementDO, T> getter) {
|
||||
return requirement == null ? null : getter.apply(requirement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅记录真实发生变化的字段,避免审计日志写入冗余内容。
|
||||
*/
|
||||
private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
|
||||
if (Objects.equals(before, after)) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> value = new LinkedHashMap<>();
|
||||
value.put("before", before);
|
||||
value.put("after", after);
|
||||
fieldChanges.put(fieldName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理可能为空的文本,去除首尾空格后若为空则返回null
|
||||
*/
|
||||
|
||||
@@ -22,12 +22,13 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -65,8 +66,9 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setReviewRequired(0); // 不需要评审
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setProposerNickname("proposer-a");
|
||||
reqVO.setCurrentHandlerUserId(2002L);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
reqVO.setCurrentHandlerUserNickname("handler-a");
|
||||
|
||||
// 模拟"全部需求"模块存在(parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
@@ -85,6 +87,12 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
assertEquals("测试需求", created.getTitle());
|
||||
assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
||||
assertEquals(100L, created.getProductId());
|
||||
assertEquals("handler-a", created.getCurrentHandlerUserNickname());
|
||||
|
||||
ArgumentCaptor<BizAuditLogDO> auditCaptor = ArgumentCaptor.forClass(BizAuditLogDO.class);
|
||||
verify(bizAuditLogMapper, times(1)).insert(auditCaptor.capture());
|
||||
assertNotNull(auditCaptor.getValue().getFieldChanges());
|
||||
assertTrue(auditCaptor.getValue().getFieldChanges().contains("currentHandlerUserNickname"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -98,7 +106,6 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setReviewRequired(1); // 需要评审
|
||||
reqVO.setPriority(2);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
// 模拟"全部需求"模块存在(parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
@@ -130,7 +137,6 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setReviewRequired(0);
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
|
||||
@@ -155,7 +161,6 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setReviewRequired(0);
|
||||
reqVO.setPriority(2);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
// 模拟"全部需求"模块存在(未选择模块时自动归属,parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
@@ -303,7 +308,6 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setTitle("子需求");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
when(requirementMapper.updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null))
|
||||
@@ -320,7 +324,7 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
assertEquals(parentId, child.getParentId());
|
||||
assertEquals("pending_dispatch", child.getStatusCode());
|
||||
assertEquals(parent.getModuleId(), child.getModuleId());
|
||||
assertEquals(parent.getProposerId(), child.getProposerId());
|
||||
assertEquals(reqVO.getProposerId(), child.getProposerId());
|
||||
|
||||
// 验证父需求状态变为实施中
|
||||
verify(requirementMapper, times(1))
|
||||
@@ -342,7 +346,6 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setTitle("子需求2");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
|
||||
@@ -366,7 +369,6 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setTitle("子需求");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
|
||||
|
||||
@@ -4,11 +4,16 @@ 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.user.UserSimpleRespVO;
|
||||
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.convert.user.UserConvert;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.dept.DeptDO;
|
||||
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.service.dept.DeptService;
|
||||
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -22,9 +27,11 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
/**
|
||||
* 用户管理链路 Controller
|
||||
@@ -43,6 +50,9 @@ import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
@Validated
|
||||
public class UserManagementRelationController {
|
||||
|
||||
@Resource
|
||||
private DeptService deptService;
|
||||
|
||||
@Resource
|
||||
private UserManagementRelationService userManagementRelationService;
|
||||
|
||||
@@ -147,6 +157,19 @@ public class UserManagementRelationController {
|
||||
return success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未绑定直属上级的候选下级用户列表
|
||||
* @return 候选下级用户列表
|
||||
*/
|
||||
@GetMapping("/candidate-users")
|
||||
@Operation(summary = "获取未绑定直属上级的候选下级用户列表")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
|
||||
public CommonResult<List<UserSimpleRespVO>> getCandidateSubordinateUsers() {
|
||||
List<AdminUserDO> users = userManagementRelationService.getCandidateSubordinateUsers();
|
||||
Map<Long, DeptDO> deptMap = deptService.getDeptMap(convertList(users, AdminUserDO::getDeptId));
|
||||
return success(UserConvert.INSTANCE.convertSimpleList(users, deptMap));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户管理链路树形结构
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.AdminUserDO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -99,6 +100,13 @@ public interface UserManagementRelationService {
|
||||
*/
|
||||
List<UserManagementRelationTreeRespVO> getRelationTree(UserManagementRelationQueryReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 获取还未绑定直属上级的候选下级用户列表
|
||||
*
|
||||
* @return 候选下级用户列表
|
||||
*/
|
||||
List<AdminUserDO> getCandidateSubordinateUsers();
|
||||
|
||||
/**
|
||||
* 获得用户管理链路 Map
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@ 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.enums.CommonStatusEnum;
|
||||
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;
|
||||
@@ -243,7 +244,7 @@ public class UserManagementRelationServiceImpl implements UserManagementRelation
|
||||
|
||||
Long targetUserId = determineTargetUserId(reqVO, context);
|
||||
if (targetUserId == null) {
|
||||
return buildFullTree(context);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return buildTargetBranchTree(targetUserId, context);
|
||||
@@ -272,6 +273,32 @@ public class UserManagementRelationServiceImpl implements UserManagementRelation
|
||||
return userManagementRelationMapper.selectListBySubordinateUserId(subordinateUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取候选的下属用户列表
|
||||
* @return 候选的下属用户列表
|
||||
*/
|
||||
@Override
|
||||
public List<AdminUserDO> getCandidateSubordinateUsers() {
|
||||
List<AdminUserDO> users = adminUserService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
if (CollUtil.isEmpty(users)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<UserManagementRelationDO> relations =
|
||||
userManagementRelationMapper.selectList(new UserManagementRelationQueryReqVO());
|
||||
Set<Long> boundSubordinateUserIds = relations.stream()
|
||||
.map(UserManagementRelationDO::getSubordinateUserId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return users.stream()
|
||||
.filter(adminUserService::isUserAvailable)
|
||||
.filter(user -> !boundSubordinateUserIds.contains(user.getId()))
|
||||
.sorted(Comparator.comparing(AdminUserDO::getDeptId, Comparator.nullsLast(Long::compareTo))
|
||||
.thenComparing(AdminUserDO::getNickname, Comparator.nullsLast(String::compareTo))
|
||||
.thenComparing(AdminUserDO::getId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户管理链路树形结构
|
||||
* 业务逻辑:
|
||||
@@ -402,112 +429,50 @@ public class UserManagementRelationServiceImpl implements UserManagementRelation
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建目标用户相关的分支树形结构
|
||||
* 只包含目标用户的上级链和下级树,不包含同级节点
|
||||
* 构建目标用户相关的分支树形结构(搜索模式)
|
||||
* 只包含目标用户的上级链和目标用户本身,不显示下级
|
||||
* <p>
|
||||
* 构建逻辑:
|
||||
* 1. 从目标用户向上追溯到根节点,记录上级链
|
||||
* 2. 从根节点开始,只沿着包含目标用户的分支向下构建
|
||||
* 3. 构建目标用户的所有下级
|
||||
* 3. 目标用户节点不构建下级
|
||||
*
|
||||
* @param targetUserId 目标用户ID
|
||||
* @param context 树形结构上下文
|
||||
* @return 目标用户相关的分支树形结构列表
|
||||
*/
|
||||
private List<UserManagementRelationTreeRespVO> buildTargetBranchTree(Long targetUserId, TreeBuildContext context) {
|
||||
LinkedList<Long> pathToRoot = findPathToRoot(targetUserId, context);
|
||||
if (pathToRoot.isEmpty()) {
|
||||
AdminUserDO targetUser = context.getUserMap().get(targetUserId);
|
||||
if (targetUser == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Long rootUserId = pathToRoot.getFirst();
|
||||
UserManagementRelationTreeRespVO rootNode = buildRootNode(rootUserId, context);
|
||||
if (rootNode == null) {
|
||||
UserManagementRelationDO targetRelation = context.getSubordinateToRelationMap().get(targetUserId);
|
||||
Long targetRelationId = targetRelation != null ? targetRelation.getId() : null;
|
||||
if (targetRelation == null || Objects.equals(targetRelation.getManagerUserId(), targetUserId)) {
|
||||
// 根节点搜索时只返回本人,不继续展示更高层级
|
||||
UserManagementRelationTreeRespVO targetNode =
|
||||
buildTreeNodeForSearch(targetRelationId, targetUserId, targetUserId, context);
|
||||
return targetNode == null ? Collections.emptyList() : Collections.singletonList(targetNode);
|
||||
}
|
||||
|
||||
Long managerUserId = targetRelation.getManagerUserId();
|
||||
UserManagementRelationDO managerRelation = context.getSubordinateToRelationMap().get(managerUserId);
|
||||
Long managerRelationId = managerRelation != null ? managerRelation.getId() : null;
|
||||
UserManagementRelationTreeRespVO managerNode =
|
||||
buildTreeNodeForSearch(managerRelationId, managerUserId, managerUserId, context);
|
||||
if (managerNode == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (pathToRoot.size() > 1) {
|
||||
buildBranchPath(rootNode, pathToRoot, context);
|
||||
UserManagementRelationTreeRespVO targetNode =
|
||||
buildTreeNodeForSearch(targetRelationId, targetUserId, managerUserId, context);
|
||||
if (targetNode == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
managerNode.setChildren(Collections.singletonList(targetNode));
|
||||
return Collections.singletonList(managerNode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -611,6 +576,40 @@ public class UserManagementRelationServiceImpl implements UserManagementRelation
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树形节点(搜索模式)
|
||||
* 与buildTreeNode的区别:不构建下级节点,只返回当前节点信息
|
||||
*
|
||||
* @param relationId 关系记录主键ID
|
||||
* @param userId 当前用户ID
|
||||
* @param managerUserId 上级用户ID
|
||||
* @param context 树形结构上下文
|
||||
* @return 树形节点(无下级)
|
||||
*/
|
||||
private UserManagementRelationTreeRespVO buildTreeNodeForSearch(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());
|
||||
}
|
||||
}
|
||||
|
||||
node.setChildren(Collections.emptyList());
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一时间有效性过滤条件:
|
||||
* <p>
|
||||
|
||||
Reference in New Issue
Block a user