diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java index 46a32c9..80854e6 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java @@ -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 = "

详细描述需求内容

") + @Schema(description = "需求描述,支持富文本", example = "

详细描述需求内容

") 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 children; - @Schema(description = "是否为终态(已拒绝、已取消、已关闭)", example = "false") + @Schema(description = "是否为终态,已拒绝、已取消、已关闭都算终态", example = "false") private Boolean terminal; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSaveReqVO.java index cc2798b..e626b22 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSaveReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSaveReqVO.java @@ -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 = "

详细描述需求内容

") + @Schema(description = "需求描述,支持富文本", example = "

详细描述需求内容

") 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; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSplitReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSplitReqVO.java index bc975ae..cb88c9b 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSplitReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSplitReqVO.java @@ -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 = "

详细描述需求内容

") + @Schema(description = "需求描述,支持富文本", example = "

详细描述需求内容

") 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; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementUpdateReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementUpdateReqVO.java index 49f5e27..1b7d7b8 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementUpdateReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementUpdateReqVO.java @@ -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 = "

详细描述需求内容

") + @Schema(description = "需求描述,支持富文本", example = "

详细描述需求内容

") 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; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java index e81795c..05bf62f 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java @@ -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; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java index dc4d105..0659e42 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java @@ -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 CHILD_ALLOW_CLOSE_STATUSES = List.of(STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED); // 允许删除的状态集合(实施中之前的状态) private static final List ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH); + // 父需求取消时,子需求允许的状态集合(仅已拒绝和已取消) + private static final List 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 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 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 valueOf(ProductRequirementDO requirement, Function getter) { + return requirement == null ? null : getter.apply(requirement); + } + + /** + * 仅记录真实发生变化的字段,避免审计日志写入冗余内容。 + */ + private void appendFieldChange(Map fieldChanges, String fieldName, Object before, Object after) { + if (Objects.equals(before, after)) { + return; + } + Map value = new LinkedHashMap<>(); + value.put("before", before); + value.put("after", after); + fieldChanges.put(fieldName, value); + } + /** * 处理可能为空的文本,去除首尾空格后若为空则返回null */ diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java index 45d2889..d20f700 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java @@ -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 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); diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/UserManagementRelationController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/UserManagementRelationController.java index 934ece9..96e73d6 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/UserManagementRelationController.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/UserManagementRelationController.java @@ -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> getCandidateSubordinateUsers() { + List users = userManagementRelationService.getCandidateSubordinateUsers(); + Map deptMap = deptService.getDeptMap(convertList(users, AdminUserDO::getDeptId)); + return success(UserConvert.INSTANCE.convertSimpleList(users, deptMap)); + } + /** * 获取用户管理链路树形结构 * diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserManagementRelationService.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserManagementRelationService.java index d4bc022..8c29891 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserManagementRelationService.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserManagementRelationService.java @@ -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 getRelationTree(UserManagementRelationQueryReqVO reqVO); + /** + * 获取还未绑定直属上级的候选下级用户列表 + * + * @return 候选下级用户列表 + */ + List getCandidateSubordinateUsers(); + /** * 获得用户管理链路 Map * diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserManagementRelationServiceImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserManagementRelationServiceImpl.java index 2ec234d..a0a14d5 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserManagementRelationServiceImpl.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserManagementRelationServiceImpl.java @@ -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 getCandidateSubordinateUsers() { + List users = adminUserService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus()); + if (CollUtil.isEmpty(users)) { + return Collections.emptyList(); + } + + List relations = + userManagementRelationMapper.selectList(new UserManagementRelationQueryReqVO()); + Set 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 } /** - * 构建目标用户相关的分支树形结构 - * 只包含目标用户的上级链和下级树,不包含同级节点 + * 构建目标用户相关的分支树形结构(搜索模式) + * 只包含目标用户的上级链和目标用户本身,不显示下级 *

* 构建逻辑: * 1. 从目标用户向上追溯到根节点,记录上级链 * 2. 从根节点开始,只沿着包含目标用户的分支向下构建 - * 3. 构建目标用户的所有下级 + * 3. 目标用户节点不构建下级 * * @param targetUserId 目标用户ID * @param context 树形结构上下文 * @return 目标用户相关的分支树形结构列表 */ private List buildTargetBranchTree(Long targetUserId, TreeBuildContext context) { - LinkedList 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 findPathToRoot(Long targetUserId, TreeBuildContext context) { - LinkedList path = new LinkedList<>(); - Set 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 pathToRoot, TreeBuildContext context) { - if (pathToRoot.size() <= 1) { - return; - } - - Long parentUserId = parentNode.getUserId(); - List 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 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; + } + /** * 统一时间有效性过滤条件: *