diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java index 14bd567..18d9900 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java @@ -81,8 +81,6 @@ public class ProductRequirementController { @PostMapping("/split") @Operation(summary = "拆分产品需求") public CommonResult splitRequirement(@Valid @RequestBody ProductRequirementSplitReqVO reqVO) { - System.out.println("-----------------------"); - System.out.println(reqVO); return success(requirementService.splitRequirement(reqVO)); } @@ -103,6 +101,16 @@ public class ProductRequirementController { return success(requirementService.getAllowedTransitions(requirementId, productId)); } + @GetMapping("/has-dispatched") + @Operation(summary = "判断产品需求是否已分流生成项目需求") + @Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024") + @Parameter(name = "productId", description = "产品编号", required = true, example = "1024") + public CommonResult hasDispatchedProjectRequirement( + @RequestParam("requirementId") Long requirementId, + @RequestParam("productId") Long productId) { + return success(requirementService.hasDispatchedProjectRequirement(requirementId, productId)); + } + @GetMapping("/lifecycle") @Operation(summary = "获取需求生命周期信息") @Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024") @@ -113,8 +121,15 @@ public class ProductRequirementController { return success(requirementService.getRequirementLifecycle(requirementId, productId)); } - // ========== 模块管理 ========== + @GetMapping("/dispatched-project-link") + @Operation(summary = "获取产品需求分流后对应的项目需求跳转链接") + @Parameter(name = "productRequirementId", description = "产品需求编号", required = true, example = "1024") + public CommonResult getDispatchedProjectLink( + @RequestParam("productRequirementId") Long productRequirementId) { + return success(requirementService.getDispatchedProjectLink(productRequirementId)); + } + // ========== 模块管理 ========== @PostMapping("/module/create") @Operation(summary = "创建需求模块") public CommonResult createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDispatchedProjectLinkRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDispatchedProjectLinkRespVO.java new file mode 100644 index 0000000..068d61a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDispatchedProjectLinkRespVO.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 管理后台 - 产品需求分流后项目需求跳转链接 Response VO + */ +@Schema(description = "管理后台 - 产品需求分流后项目需求跳转链接 Response VO") +@Data +public class ProductRequirementDispatchedProjectLinkRespVO { + + @Schema(description = "项目需求ID", example = "10086") + private Long projectRequirementId; + + @Schema(description = "实现项目ID", example = "8888") + private Long projectId; + +} \ No newline at end of file diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java new file mode 100644 index 0000000..3634bca --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java @@ -0,0 +1,164 @@ +package com.njcn.rdms.module.project.controller.admin.project; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementDeleteReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementLifecycleRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleDeleteReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSplitReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusDictRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusTransitionRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementUpdateReqVO; +import com.njcn.rdms.module.project.service.project.ProjectRequirementService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +/** + * 管理后台 - 项目需求控制器 + */ +@Tag(name = "管理后台 - 项目需求") +@RestController +@RequestMapping("/project/project/requirement") +@Validated +public class ProjectRequirementController { + + @Resource + private ProjectRequirementService requirementService; + + @PostMapping("/create") + @Operation(summary = "创建项目需求") + public CommonResult createRequirement(@Valid @RequestBody ProjectRequirementSaveReqVO createReqVO) { + return success(requirementService.createRequirement(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新项目需求") + public CommonResult updateRequirement(@Valid @RequestBody ProjectRequirementUpdateReqVO updateReqVO) { + requirementService.updateRequirement(updateReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取需求详情") + @Parameter(name = "id", description = "需求编号", required = true, example = "1024") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + public CommonResult getRequirement(@RequestParam("id") Long id, + @RequestParam("projectId") Long projectId) { + return success(requirementService.getRequirement(id, projectId)); + } + + @GetMapping("/page") + @Operation(summary = "获取需求分页列表") + public CommonResult> getRequirementPage(@Valid ProjectRequirementPageReqVO pageReqVO) { + return success(requirementService.getRequirementPage(pageReqVO)); + } + + @GetMapping("/tree") + @Operation(summary = "获取需求树形列表") + public CommonResult> getRequirementTree(@Valid ProjectRequirementPageReqVO pageReqVO) { + return success(requirementService.getRequirementTree(pageReqVO)); + } + + @PostMapping("/change-status") + @Operation(summary = "变更需求状态") + public CommonResult changeRequirementStatus(@Valid @RequestBody ProjectRequirementStatusActionReqVO reqVO) { + requirementService.changeRequirementStatus(reqVO); + return success(true); + } + + @PostMapping("/delete") + @Operation(summary = "删除项目需求") + public CommonResult deleteRequirement(@Valid @RequestBody ProjectRequirementDeleteReqVO reqVO) { + requirementService.deleteRequirement(reqVO.getId(), reqVO.getProjectId()); + return success(true); + } + + @PostMapping("/split") + @Operation(summary = "拆分项目需求") + public CommonResult splitRequirement(@Valid @RequestBody ProjectRequirementSplitReqVO reqVO) { + return success(requirementService.splitRequirement(reqVO)); + } + + @PostMapping("/close") + @Operation(summary = "关闭项目需求") + public CommonResult closeRequirement(@Valid @RequestBody ProjectRequirementCloseReqVO reqVO) { + requirementService.closeRequirement(reqVO); + return success(true); + } + + @GetMapping("/allowed-transitions") + @Operation(summary = "获取需求可执行的状态动作列表") + @Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + public CommonResult> getAllowedTransitions( + @RequestParam("requirementId") Long requirementId, + @RequestParam("projectId") Long projectId) { + return success(requirementService.getAllowedTransitions(requirementId, projectId)); + } + + @GetMapping("/lifecycle") + @Operation(summary = "获取需求生命周期信息") + @Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + public CommonResult getRequirementLifecycle( + @RequestParam("requirementId") Long requirementId, + @RequestParam("projectId") Long projectId) { + return success(requirementService.getRequirementLifecycle(requirementId, projectId)); + } + + @PostMapping("/module/create") + @Operation(summary = "创建需求模块") + public CommonResult createRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) { + return success(requirementService.createRequirementModule(reqVO)); + } + + @PutMapping("/module/update") + @Operation(summary = "更新需求模块") + public CommonResult updateRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) { + requirementService.updateRequirementModule(reqVO); + return success(true); + } + + @PostMapping("/module/delete") + @Operation(summary = "删除需求模块") + public CommonResult deleteRequirementModule(@Valid @RequestBody ProjectRequirementModuleDeleteReqVO reqVO) { + requirementService.deleteRequirementModule(reqVO.getId(), reqVO.getProjectId()); + return success(true); + } + + @GetMapping("/module/tree") + @Operation(summary = "获取需求模块树") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + public CommonResult> getRequirementModuleTree(@RequestParam("projectId") Long projectId) { + return success(requirementService.getRequirementModuleTree(projectId)); + } + + @GetMapping("/status/dict") + @Operation(summary = "获取需求所有状态字典") + public CommonResult> getRequirementStatusDict() { + return success(requirementService.getRequirementStatusDict()); + } + + @GetMapping("/status/dict/terminal") + @Operation(summary = "获取需求终态状态字典") + public CommonResult> getRequirementTerminalStatusDict() { + return success(requirementService.getRequirementTerminalStatusDict()); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementCloseReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementCloseReqVO.java new file mode 100644 index 0000000..6af44a2 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementCloseReqVO.java @@ -0,0 +1,29 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 项目需求关闭 Request VO + */ +@Schema(description = "管理后台 - 项目需求关闭 Request VO") +@Data +public class ProjectRequirementCloseReqVO { + + @Schema(description = "需求 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "需求 ID 不能为空") + private Long id; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目 ID 不能为空") + private Long projectId; + + @Schema(description = "关闭原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "需求已完成验收") + @NotBlank(message = "关闭原因不能为空") + @Size(max = 255, message = "关闭原因长度不能超过 255 个字符") + private String reason; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementDeleteReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementDeleteReqVO.java new file mode 100644 index 0000000..f421579 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementDeleteReqVO.java @@ -0,0 +1,21 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 管理后台 - 项目需求删除 Request VO + */ +@Schema(description = "管理后台 - 项目需求删除 Request VO") +@Data +public class ProjectRequirementDeleteReqVO { + + @Schema(description = "需求 ID", example = "1024") + private Long id; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目 ID 不能为空") + private Long projectId; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementLifecycleRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementLifecycleRespVO.java new file mode 100644 index 0000000..3192ac0 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementLifecycleRespVO.java @@ -0,0 +1,33 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 管理后台 - 项目需求生命周期 Response VO + */ +@Schema(description = "管理后台 - 项目需求生命周期 Response VO") +@Data +public class ProjectRequirementLifecycleRespVO { + + @Schema(description = "当前状态编码", example = "implementing") + private String statusCode; + + @Schema(description = "当前状态名称", example = "实施中") + private String statusName; + + @Schema(description = "最近一次状态动作原因", example = "评审通过") + private String lastStatusReason; + + @Schema(description = "是否终态", example = "false") + private Boolean terminal; + + @Schema(description = "是否允许编辑", example = "true") + private Boolean allowEdit; + + @Schema(description = "当前状态可执行动作列表") + private List availableActions; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementModuleDeleteReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementModuleDeleteReqVO.java new file mode 100644 index 0000000..1cecccc --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementModuleDeleteReqVO.java @@ -0,0 +1,21 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 管理后台 - 项目需求模块删除 Request VO + */ +@Schema(description = "管理后台 - 项目需求模块删除 Request VO") +@Data +public class ProjectRequirementModuleDeleteReqVO { + + @Schema(description = "模块 ID", example = "1024") + private Long id; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目 ID 不能为空") + private Long projectId; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementModuleReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementModuleReqVO.java new file mode 100644 index 0000000..029ba61 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementModuleReqVO.java @@ -0,0 +1,40 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 项目需求模块保存 Request VO + */ +@Schema(description = "管理后台 - 项目需求模块保存 Request VO") +@Data +public class ProjectRequirementModuleReqVO { + + @Schema(description = "模块 ID", example = "1024") + private Long id; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目 ID 不能为空") + private Long projectId; + + @Schema(description = "父模块 ID,0 表示顶级", example = "0") + private Long parentId; + + @Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能") + @NotBlank(message = "模块名称不能为空") + @Size(max = 100, message = "模块名称长度不能超过 100 个字符") + private String moduleName; + + @Schema(description = "模块说明", example = "项目核心功能模块") + private String remark; + + @Schema(description = "图标", example = "icon-function") + private String icon; + + @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/project/vo/requirement/ProjectRequirementModuleRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementModuleRespVO.java new file mode 100644 index 0000000..20483a7 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementModuleRespVO.java @@ -0,0 +1,39 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 管理后台 - 项目需求模块 Response VO + */ +@Schema(description = "管理后台 - 项目需求模块 Response VO") +@Data +public class ProjectRequirementModuleRespVO { + + @Schema(description = "模块 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "父模块 ID,0 表示顶级", example = "0") + private Long parentId; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long projectId; + + @Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能") + private String moduleName; + + @Schema(description = "模块说明", example = "项目核心功能模块") + private String remark; + + @Schema(description = "图标", example = "icon-function") + private String icon; + + @Schema(description = "排序值", example = "0") + private Integer sort; + + @Schema(description = "子模块列表") + private List children; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementPageReqVO.java new file mode 100644 index 0000000..8e3ec83 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementPageReqVO.java @@ -0,0 +1,48 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import com.njcn.rdms.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 管理后台 - 项目需求分页 Request VO + */ +@Schema(description = "管理后台 - 项目需求分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectRequirementPageReqVO extends PageParam { + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long projectId; + + @Schema(description = "所属模块 ID", example = "1024") + private Long moduleId; + + @Schema(description = "所属模块 ID 列表,包含子模块,用于 IN 查询", example = "[1024, 1025]") + private List moduleIds; + + @Schema(description = "父需求 ID,查询子需求时使用", example = "1024") + private Long parentId; + + @Schema(description = "标题关键字", example = "模块") + private String title; + + @Schema(description = "需求分类字典值", example = "function") + private String category; + + @Schema(description = "优先级", example = "1") + private Integer priority; + + @Schema(description = "状态编码", example = "implementing") + private String statusCode; + + @Schema(description = "当前处理人用户 ID", example = "1024") + private Long currentHandlerUserId; + + @Schema(description = "来源类型", example = "manual") + private String sourceType; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementRespVO.java new file mode 100644 index 0000000..586b061 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementRespVO.java @@ -0,0 +1,94 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 管理后台 - 项目需求 Response VO + */ +@Schema(description = "管理后台 - 项目需求 Response VO") +@Data +public class ProjectRequirementRespVO { + + @Schema(description = "需求 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "父需求 ID,0 表示顶级需求", example = "0") + private Long parentId; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long projectId; + + @Schema(description = "所属模块 ID", example = "1024") + private Long moduleId; + + @Schema(description = "是否需要评审,0 不需要,1 需要", example = "0") + private Integer reviewRequired; + + @Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理") + private String title; + + @Schema(description = "需求描述,支持富文本", example = "

详细描述需求内容

") + private String description; + + @Schema(description = "需求分类字典值", example = "function") + private String category; + + @Schema(description = "需求分类名称", example = "功能需求") + private String categoryName; + + @Schema(description = "需求来源类型", example = "manual") + private String sourceType; + + @Schema(description = "来源业务 ID", example = "1024") + private Long sourceBizId; + + @Schema(description = "优先级", example = "1") + private Integer priority; + + @Schema(description = "优先级名称", example = "中") + private String priorityName; + + @Schema(description = "当前状态编码", example = "implementing") + private String statusCode; + + @Schema(description = "当前状态名称", example = "实施中") + private String statusName; + + @Schema(description = "最近一次状态动作原因", example = "评审通过") + private String lastStatusReason; + + @Schema(description = "提出人用户 ID", example = "1024") + private Long proposerId; + + @Schema(description = "提出人昵称", example = "张三") + private String proposerNickname; + + @Schema(description = "所需工时", example = "8") + private Double workHours; + + @Schema(description = "当前处理人用户 ID", example = "1024") + private Long currentHandlerUserId; + + @Schema(description = "当前处理人昵称", example = "李四") + private String currentHandlerUserNickname; + + @Schema(description = "排序值", example = "0") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "子需求列表,树形结构") + private List children; + + @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/project/vo/requirement/ProjectRequirementSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementSaveReqVO.java new file mode 100644 index 0000000..5651cde --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementSaveReqVO.java @@ -0,0 +1,67 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 项目需求保存 Request VO + */ +@Schema(description = "管理后台 - 项目需求保存 Request VO") +@Data +public class ProjectRequirementSaveReqVO { + + @Schema(description = "需求 ID", example = "1024") + private Long id; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目 ID 不能为空") + private Long projectId; + + @Schema(description = "所属模块 ID,为空时归入全部需求模块", example = "1024") + private Long moduleId; + + @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 个字符") + private String title; + + @Schema(description = "需求描述,支持富文本", example = "

详细描述需求内容

") + private String description; + + @Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function") + @NotBlank(message = "需求分类不能为空") + @Size(max = 64, message = "需求分类长度不能超过 64 个字符") + private String category; + + @Schema(description = "优先级,0 低、1 中、2 高、3 紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优先级不能为空") + private Integer priority; + + @Schema(description = "提出人用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "提出人不能为空") + private Long proposerId; + + @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 = "0") + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementSplitReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementSplitReqVO.java new file mode 100644 index 0000000..422f6a1 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementSplitReqVO.java @@ -0,0 +1,68 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 项目需求拆分 Request VO + */ +@Schema(description = "管理后台 - 项目需求拆分 Request VO") +@Data +public class ProjectRequirementSplitReqVO { + + @Schema(description = "父需求 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "父需求 ID 不能为空") + private Long parentId; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目 ID 不能为空") + private Long projectId; + + @Schema(description = "所属模块 ID,为空时继承父需求模块", example = "1024") + private Long moduleId; + + @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 个字符") + private String title; + + @Schema(description = "需求描述,支持富文本", example = "

详细描述需求内容

") + private String description; + + @Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function") + @NotBlank(message = "需求分类不能为空") + @Size(max = 64, message = "需求分类长度不能超过 64 个字符") + private String category; + + @Schema(description = "优先级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优先级不能为空") + private Integer priority; + + @Schema(description = "提出人用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "提出人不能为空") + private Long proposerId; + + @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 = "0") + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusActionReqVO.java new file mode 100644 index 0000000..d2eb1c8 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusActionReqVO.java @@ -0,0 +1,32 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 项目需求状态变更 Request VO + */ +@Schema(description = "管理后台 - 项目需求状态变更 Request VO") +@Data +public class ProjectRequirementStatusActionReqVO { + + @Schema(description = "需求 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "需求 ID 不能为空") + private Long id; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目 ID 不能为空") + private Long projectId; + + @Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pass_review") + @NotBlank(message = "动作编码不能为空") + @Size(max = 32, message = "动作编码长度不能超过 32 个字符") + private String actionCode; + + @Schema(description = "状态变更原因", example = "评审通过") + private String reason; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusDictRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusDictRespVO.java new file mode 100644 index 0000000..0cfb622 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusDictRespVO.java @@ -0,0 +1,28 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 管理后台 - 项目需求状态字典 Response VO + */ +@Schema(description = "管理后台 - 项目需求状态字典 Response VO") +@Data +public class ProjectRequirementStatusDictRespVO { + + @Schema(description = "状态编码", example = "pending_confirm") + private String statusCode; + + @Schema(description = "状态名称", example = "待确认") + private String statusName; + + @Schema(description = "排序值", example = "1") + private Integer sort; + + @Schema(description = "是否初始状态", example = "true") + private Boolean initialFlag; + + @Schema(description = "是否终态", example = "false") + private Boolean terminalFlag; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusTransitionRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusTransitionRespVO.java new file mode 100644 index 0000000..1af41d4 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusTransitionRespVO.java @@ -0,0 +1,30 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 管理后台 - 项目需求状态可执行动作 Response VO + */ +@Schema(description = "管理后台 - 项目需求状态可执行动作 Response VO") +@Data +@NoArgsConstructor +public class ProjectRequirementStatusTransitionRespVO { + + @Schema(description = "动作编码", example = "pass_review") + private String actionCode; + + @Schema(description = "动作名称", example = "评审通过") + private String actionName; + + @Schema(description = "目标状态编码", example = "implementing") + private String toStatusCode; + + @Schema(description = "目标状态名称", example = "实施中") + private String toStatusName; + + @Schema(description = "是否必须填写原因", example = "false") + private Boolean needReason; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementUpdateReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementUpdateReqVO.java new file mode 100644 index 0000000..2616212 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementUpdateReqVO.java @@ -0,0 +1,68 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 项目需求编辑 Request VO + */ +@Schema(description = "管理后台 - 项目需求编辑 Request VO") +@Data +public class ProjectRequirementUpdateReqVO { + + @Schema(description = "需求 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "需求 ID 不能为空") + private Long id; + + @Schema(description = "所属项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目 ID 不能为空") + private Long projectId; + + @Schema(description = "所属模块 ID,为空时归入全部需求模块", example = "1024") + private Long moduleId; + + @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 个字符") + private String title; + + @Schema(description = "需求描述,支持富文本", example = "

详细描述需求内容

") + private String description; + + @Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function") + @NotBlank(message = "需求分类不能为空") + @Size(max = 64, message = "需求分类长度不能超过 64 个字符") + private String category; + + @Schema(description = "优先级,0 低、1 中、2 高、3 紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优先级不能为空") + private Integer priority; + + @Schema(description = "提出人用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "提出人不能为空") + private Long proposerId; + + @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 = "0") + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementDO.java new file mode 100644 index 0000000..38d21b0 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementDO.java @@ -0,0 +1,99 @@ +package com.njcn.rdms.module.project.dal.dataobject.project; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 项目需求主表 + */ +@TableName("rdms_project_requirement") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectRequirementDO extends BaseDO { + + /** + * 主键 ID + */ + @TableId + private Long id; + /** + * 父需求 ID,0 表示顶级需求 + */ + private Long parentId; + /** + * 所属项目 ID + */ + private Long projectId; + /** + * 所属模块 ID + */ + private Long moduleId; + /** + * 产品需求id + */ + private Long productRequirementId; + /** + * 是否需要评审,0 不需要,1 需要 + */ + private Integer reviewRequired; + /** + * 需求标题 + */ + private String title; + /** + * 需求描述,支持富文本 + */ + private String description; + /** + * 需求分类字典值 + */ + private String category; + /** + * 来源类型 + */ + private String sourceType; + /** + * 来源业务 ID + */ + private Long sourceBizId; + /** + * 优先级 + */ + private Integer priority; + /** + * 当前状态编码 + */ + private String statusCode; + /** + * 最近一次状态动作原因 + */ + private String lastStatusReason; + /** + * 提出人用户 ID + */ + private Long proposerId; + /** + * 提出人昵称快照 + */ + private String proposerNickname; + /** + * 当前处理人用户 ID + */ + private Long currentHandlerUserId; + /** + * 当前处理人昵称快照 + */ + private String currentHandlerUserNickname; + /** + * 预估工时 + */ + private Double workHours; + /** + * 排序值 + */ + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementModuleDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementModuleDO.java new file mode 100644 index 0000000..9884817 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementModuleDO.java @@ -0,0 +1,47 @@ +package com.njcn.rdms.module.project.dal.dataobject.project; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 项目需求模块表 + */ +@TableName("rdms_project_requirement_module") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectRequirementModuleDO extends BaseDO { + + /** + * 主键 ID + */ + @TableId + private Long id; + /** + * 父模块 ID,0 表示顶级模块 + */ + private Long parentId; + /** + * 所属项目 ID + */ + private Long projectId; + /** + * 模块名称 + */ + private String moduleName; + /** + * 模块说明 + */ + private String remark; + /** + * 图标 + */ + private String icon; + /** + * 排序值 + */ + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementStatusLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementStatusLogDO.java new file mode 100644 index 0000000..c3c2d2d --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/ProjectRequirementStatusLogDO.java @@ -0,0 +1,55 @@ +package com.njcn.rdms.module.project.dal.dataobject.project; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 项目需求状态变更日志表 + */ +@TableName("rdms_project_requirement_status_log") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProjectRequirementStatusLogDO extends BaseDO { + + /** + * 主键 ID + */ + @TableId + private Long id; + /** + * 需求 ID + */ + private Long requirementId; + /** + * 动作编码 + */ + private String actionType; + /** + * 变更前状态 + */ + private String fromStatus; + /** + * 变更后状态 + */ + private String toStatus; + /** + * 动作原因 + */ + private String reason; + /** + * 操作人 ID + */ + private Long operatorUserId; + /** + * 操作人名称快照 + */ + private String operatorName; + /** + * 需求标题快照 + */ + private String requirementTitleSnapshot; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementMapper.java new file mode 100644 index 0000000..0276e3d --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementMapper.java @@ -0,0 +1,95 @@ +package com.njcn.rdms.module.project.dal.mysql.project; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * 项目需求 Mapper + */ +@Mapper +public interface ProjectRequirementMapper extends BaseMapperX { + + /** + * 分页查询需求列表 + */ + default PageResult selectPage(ProjectRequirementPageReqVO reqVO) { + return selectPage(reqVO, buildQueryWrapper(reqVO)); + } + + /** + * 查询所有符合条件的需求列表,用于树查询 + */ + default List selectList(ProjectRequirementPageReqVO reqVO) { + return selectList(buildQueryWrapper(reqVO)); + } + + /** + * 构建查询条件 + */ + private LambdaQueryWrapperX buildQueryWrapper(ProjectRequirementPageReqVO reqVO) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + if (StringUtils.hasText(reqVO.getTitle())) { + queryWrapper.like(ProjectRequirementDO::getTitle, reqVO.getTitle()); + } + queryWrapper.eqIfPresent(ProjectRequirementDO::getCategory, reqVO.getCategory()) + .eqIfPresent(ProjectRequirementDO::getPriority, reqVO.getPriority()) + .eqIfPresent(ProjectRequirementDO::getStatusCode, reqVO.getStatusCode()) + .eqIfPresent(ProjectRequirementDO::getCurrentHandlerUserId, reqVO.getCurrentHandlerUserId()) + .eqIfPresent(ProjectRequirementDO::getSourceType, reqVO.getSourceType()) + .inIfPresent(ProjectRequirementDO::getModuleId, reqVO.getModuleIds()) + .eqIfPresent(ProjectRequirementDO::getModuleId, reqVO.getModuleId()) + .eqIfPresent(ProjectRequirementDO::getParentId, reqVO.getParentId()) + .eq(ProjectRequirementDO::getProjectId, reqVO.getProjectId()) + .orderByAsc(ProjectRequirementDO::getSort) + .orderByDesc(ProjectRequirementDO::getCreateTime); + return queryWrapper; + } + + /** + * 根据父需求 ID 查询子需求列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectRequirementDO::getParentId, parentId) + .orderByAsc(ProjectRequirementDO::getSort) + .orderByDesc(ProjectRequirementDO::getCreateTime)); + } + + /** + * 根据模块 ID 查询需求列表 + */ + default List selectListByModuleId(Long moduleId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectRequirementDO::getModuleId, moduleId) + .orderByAsc(ProjectRequirementDO::getSort)); + } + + /** + * 带并发控制的状态更新 + */ + default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) { + ProjectRequirementDO update = new ProjectRequirementDO(); + update.setStatusCode(toStatus); + update.setLastStatusReason(lastStatusReason); + return update(update, new LambdaQueryWrapperX() + .eq(ProjectRequirementDO::getId, id) + .eq(ProjectRequirementDO::getStatusCode, fromStatus)); + } + + /** + * 根据 ID 和状态删除,带并发控制 + */ + default int deleteByIdAndStatus(Long id, String statusCode) { + return delete(new LambdaQueryWrapperX() + .eq(ProjectRequirementDO::getId, id) + .eq(ProjectRequirementDO::getStatusCode, statusCode)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementModuleMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementModuleMapper.java new file mode 100644 index 0000000..2f3d054 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementModuleMapper.java @@ -0,0 +1,53 @@ +package com.njcn.rdms.module.project.dal.mysql.project; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementModuleDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 项目需求模块 Mapper + */ +@Mapper +public interface ProjectRequirementModuleMapper extends BaseMapperX { + + /** + * 根据项目 ID 查询模块列表 + */ + default List selectListByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectRequirementModuleDO::getProjectId, projectId) + .orderByAsc(ProjectRequirementModuleDO::getSort) + .orderByAsc(ProjectRequirementModuleDO::getCreateTime)); + } + + /** + * 根据父模块 ID 查询子模块列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectRequirementModuleDO::getParentId, parentId) + .orderByAsc(ProjectRequirementModuleDO::getSort)); + } + + /** + * 根据项目 ID 和模块名称查询模块 + */ + default ProjectRequirementModuleDO selectByProjectIdAndModuleName(Long projectId, String moduleName) { + return selectOne(new LambdaQueryWrapperX() + .eq(ProjectRequirementModuleDO::getProjectId, projectId) + .eq(ProjectRequirementModuleDO::getModuleName, moduleName)); + } + + /** + * 根据项目 ID 和父模块 ID 查询模块 + */ + default ProjectRequirementModuleDO selectByProjectIdAndParentId(Long projectId, Long parentId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ProjectRequirementModuleDO::getProjectId, projectId) + .eq(ProjectRequirementModuleDO::getParentId, parentId)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementStatusLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementStatusLogMapper.java new file mode 100644 index 0000000..0d7e1a9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementStatusLogMapper.java @@ -0,0 +1,25 @@ +package com.njcn.rdms.module.project.dal.mysql.project; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementStatusLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 项目需求状态变更日志 Mapper + */ +@Mapper +public interface ProjectRequirementStatusLogMapper extends BaseMapperX { + + /** + * 根据需求 ID 查询状态变更日志列表 + */ + default List selectListByRequirementId(Long requirementId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProjectRequirementStatusLogDO::getRequirementId, requirementId) + .orderByDesc(ProjectRequirementStatusLogDO::getCreateTime)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java index 81ff997..73c2614 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java @@ -88,6 +88,15 @@ public interface ProductRequirementService { */ List getAllowedTransitions(Long requirementId, Long productId); + /** + * 判断需求是否已分流并生成项目需求 + * + * @param requirementId 需求编号 + * @param productId 产品编号 + * @return 是否已分流 + */ + boolean hasDispatchedProjectRequirement(Long requirementId, Long productId); + /** * 获取需求生命周期信息(当前状态 + 可执行动作) * @@ -144,4 +153,12 @@ public interface ProductRequirementService { */ List getRequirementTerminalStatusDict(); + /** + * 获取产品需求分流后对应的项目需求跳转链接 + * + * @param productRequirementId 产品需求编号 + * @return 项目需求ID和实现项目ID + */ + ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId); + } 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 cd02e32..41c7d06 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 @@ -4,18 +4,25 @@ 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.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*; +import com.njcn.rdms.module.project.constant.ProjectObjectConstants; import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO; import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO; import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO; import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementModuleDO; import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO; import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper; +import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper; +import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper; +import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; import com.njcn.rdms.module.project.enums.ErrorCodeConstants; @@ -75,7 +82,13 @@ public class ProductRequirementServiceImpl implements ProductRequirementService private static final String ACTION_DELETE = "delete"; private static final String ACTION_SPLIT = "split"; private static final String ACTION_CLOSE = "close"; + private static final String ACTION_ACCEPT = "accept"; + private static final String ACTION_DISPATCH = "dispatch"; + private static final String ACTION_CANCEL = "cancel"; + private static final String ACTION_AUTO_DERIVE = "auto_derive"; + private static final String BIZ_TYPE_REQUIREMENT = "product_requirement"; + private static final String AUTO_DERIVE_REASON = "根据子需求状态自动推导"; @Resource private ProductRequirementMapper requirementMapper; @@ -89,6 +102,12 @@ public class ProductRequirementServiceImpl implements ProductRequirementService private ObjectStatusTransitionMapper statusTransitionMapper; @Resource private ObjectStatusModelMapper statusModelMapper; + @Resource + private ProjectRequirementMapper projectRequirementMapper; + @Resource + private ProjectRequirementModuleMapper projectRequirementModuleMapper; + @Resource + private UserObjectRoleMapper userObjectRoleMapper; // ========== 需求增删改查 ========== @@ -190,8 +209,9 @@ public class ProductRequirementServiceImpl implements ProductRequirementService } } PageResult pageResult = requirementMapper.selectPage(pageReqVO); + Map statusModelMap = getStatusModelMap(); List list = pageResult.getList().stream() - .map(this::buildRequirementRespVO) + .map(requirement -> buildRequirementRespVO(requirement, statusModelMap)) .collect(Collectors.toList()); return new PageResult<>(list, pageResult.getTotal()); } @@ -217,12 +237,13 @@ public class ProductRequirementServiceImpl implements ProductRequirementService return new PageResult<>(Collections.emptyList(), 0L); } + Map statusModelMap = getStatusModelMap(); + // 第二步:找出所有匹配需求的根节点ID,同时收集路径上的所有节点ID Set rootIds = new HashSet<>(); Set pathNodeIds = new HashSet<>(); - Map requirementCache = new HashMap<>(); + Map requirementCache = buildRequirementCacheWithAncestors(matchedRequirements); for (ProductRequirementDO req : matchedRequirements) { - requirementCache.put(req.getId(), req); pathNodeIds.add(req.getId()); Long rootId = findRootRequirementIdAndCollectPath(req, requirementCache, pathNodeIds); rootIds.add(rootId); @@ -250,10 +271,11 @@ public class ProductRequirementServiceImpl implements ProductRequirementService } List pagedRootRequirements = rootRequirements.subList(fromIndex, toIndex); + Map> childrenMap = buildPathChildrenMap(pathNodeIds); // 第五步:构建树形结构(只包含路径上的节点) List list = pagedRootRequirements.stream() - .map(req -> buildRequirementRespVOWithPathChildren(req, pathNodeIds)) + .map(req -> buildRequirementRespVOWithPathChildren(req, pathNodeIds, childrenMap, statusModelMap)) .collect(Collectors.toList()); return new PageResult<>(list, (long) total); @@ -261,57 +283,110 @@ public class ProductRequirementServiceImpl implements ProductRequirementService /** * 向上追溯需求的根节点ID,同时收集路径上的所有节点ID + * * @param requirement 起始需求 - * @param cache 需求缓存(避免重复查询) + * @param cache 需求缓存(避免重复查询) * @param pathNodeIds 路径节点ID集合(输出参数) * @return 根节点ID(parentId = 0L 的需求ID) */ private Long findRootRequirementIdAndCollectPath(ProductRequirementDO requirement, - Map cache, - Set pathNodeIds) { - if (requirement.getParentId() == null || requirement.getParentId() == 0L) { - return requirement.getId(); - } - // 从缓存中查找父需求,如果没有则查询数据库 - ProductRequirementDO parent = cache.get(requirement.getParentId()); - if (parent == null) { - parent = requirementMapper.selectById(requirement.getParentId()); - if (parent != null) { - cache.put(parent.getId(), parent); + Map cache, + Set pathNodeIds) { + ProductRequirementDO current = requirement; + while (current.getParentId() != null && current.getParentId() != 0L) { + ProductRequirementDO parent = cache.get(current.getParentId()); + if (parent == null) { + // 父需求缺失时,保持原有容错行为:就近把当前节点视为根节点 + return current.getId(); } + pathNodeIds.add(parent.getId()); + current = parent; } - if (parent == null) { - // 父需求不存在(数据异常),返回当前需求作为根 - return requirement.getId(); - } - // 收集路径上的节点ID - pathNodeIds.add(parent.getId()); - return findRootRequirementIdAndCollectPath(parent, cache, pathNodeIds); + return current.getId(); } /** * 构建需求响应VO(只包含路径上的子需求) + * * @param requirement 需求 * @param pathNodeIds 路径节点ID集合 * @return 需求响应VO */ private ProductRequirementRespVO buildRequirementRespVOWithPathChildren(ProductRequirementDO requirement, - Set pathNodeIds) { - ProductRequirementRespVO respVO = buildRequirementRespVO(requirement); - // 查询子需求 - List allChildren = requirementMapper.selectListByParentId(requirement.getId()); - // 只保留路径上的子需求 + Set pathNodeIds, + Map> childrenMap, + Map statusModelMap) { + ProductRequirementRespVO respVO = buildRequirementRespVO(requirement, statusModelMap); + List allChildren = childrenMap.getOrDefault(requirement.getId(), Collections.emptyList()); List pathChildren = allChildren.stream() .filter(child -> pathNodeIds.contains(child.getId())) .toList(); if (!pathChildren.isEmpty()) { respVO.setChildren(pathChildren.stream() - .map(child -> buildRequirementRespVOWithPathChildren(child, pathNodeIds)) + .map(child -> buildRequirementRespVOWithPathChildren(child, pathNodeIds, childrenMap, statusModelMap)) .collect(Collectors.toList())); } return respVO; } + /** + * 批量补齐命中需求向上的祖先链,避免树查询逐条回库查父节点。 + */ + private Map buildRequirementCacheWithAncestors(List matchedRequirements) { + Map cache = matchedRequirements.stream() + .collect(Collectors.toMap(ProductRequirementDO::getId, Function.identity(), (left, right) -> left, HashMap::new)); + + Set pendingParentIds = matchedRequirements.stream() + .map(ProductRequirementDO::getParentId) + .filter(parentId -> parentId != null && parentId != 0L && !cache.containsKey(parentId)) + .collect(Collectors.toSet()); + + while (!pendingParentIds.isEmpty()) { + List parentRequirements = requirementMapper.selectBatchIds(pendingParentIds); + if (parentRequirements.isEmpty()) { + break; + } + Set nextPendingParentIds = new HashSet<>(); + for (ProductRequirementDO parentRequirement : parentRequirements) { + cache.putIfAbsent(parentRequirement.getId(), parentRequirement); + Long parentId = parentRequirement.getParentId(); + if (parentId != null && parentId != 0L && !cache.containsKey(parentId)) { + nextPendingParentIds.add(parentId); + } + } + pendingParentIds = nextPendingParentIds; + } + return cache; + } + + /** + * 批量加载路径节点的直属子需求,后续仅在内存中组装树结构。 + */ + private Map> buildPathChildrenMap(Set pathNodeIds) { + if (pathNodeIds.isEmpty()) { + return Collections.emptyMap(); + } + List pathChildren = requirementMapper.selectList( + new LambdaQueryWrapperX() + .in(ProductRequirementDO::getParentId, pathNodeIds) + .orderByAsc(ProductRequirementDO::getSort) + .orderByDesc(ProductRequirementDO::getCreateTime)); + Map> childrenMap = new HashMap<>(); + for (ProductRequirementDO child : pathChildren) { + childrenMap.computeIfAbsent(child.getParentId(), key -> new ArrayList<>()).add(child); + } + return childrenMap; + } + + /** + * 一次性加载当前对象类型下的状态模型,供列表和树查询复用。 + */ + private Map getStatusModelMap() { + return statusModelMapper.selectListByObjectType(REQUIREMENT_OBJECT_TYPE).stream() + .collect(Collectors.toMap(ObjectStatusModelDO::getStatusCode, Function.identity(), + (left, right) -> left, LinkedHashMap::new)); + } + /** * 判断指定模块是否为"全部需求"模块(parentId = 0L 的根模块) */ @@ -326,7 +401,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService /** * 递归获取模块及其所有子模块的ID列表 - * @param moduleId 起始模块ID + * + * @param moduleId 起始模块ID * @param productId 产品ID * @return 包含自身及所有子模块的ID列表 */ @@ -355,6 +431,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService /** * 递归获取需求及其所有子需求(包含子子需求) + * * @param requirementId 起始需求ID * @return 包含自身及所有后代需求的列表 */ @@ -398,17 +475,21 @@ public class ProductRequirementServiceImpl implements ProductRequirementService String toStatus = transition.getToStatusCode(); // accept和close动作时,校验所有子需求(包括子子需求)是否处于允许状态 - if ("accept".equals(actionCode) || "close".equals(actionCode)) { + if (ACTION_ACCEPT.equals(actionCode) || ACTION_CLOSE.equals(actionCode)) { validateAllChildrenAllowCloseOrAccept(reqVO.getId()); } - // cancel动作时,如果是父需求则校验所有子需求是否处于已拒绝或已取消 - if ("cancel".equals(actionCode) && Objects.equals(requirement.getParentId(), 0L)) { + // cancel动作时,只要存在子需求就按父需求规则校验 + if (ACTION_CANCEL.equals(actionCode) && hasChildren(requirement.getId())) { validateParentCancelAllowed(reqVO.getId()); } // close动作时,递归关闭所有已验收的子需求(包括子子需求) - if ("close".equals(actionCode)) { + if (ACTION_CLOSE.equals(actionCode)) { closeAllAcceptedChildren(reqVO.getId(), reason); } + // dispatch动作且选择了实现项目时,自动创建对应的项目需求 + if (ACTION_DISPATCH.equals(actionCode) && implementProjectId != null) { + createProjectRequirementFromProduct(requirement, implementProjectId); + } // 带并发控制的状态更新(支持同时更新实现项目ID) int updateCount = requirementMapper.updateStatusByIdAndStatusWithProject(requirement.getId(), fromStatus, toStatus, reason, implementProjectId); if (updateCount != 1) { @@ -425,6 +506,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService // 写入业务审计日志 writeBizAuditLog(requirement, actionCode, fromStatus, toStatus, buildRequirementFieldChanges(before, requirement), reason); + refreshAncestorStatusRecursively(requirement.getId()); } /** @@ -496,6 +578,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService public Long splitRequirement(ProductRequirementSplitReqVO reqVO) { // 校验父需求是否存在 ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId()); + // 产品需求一旦已分流生成项目需求,就只能到项目需求侧继续拆分 + validateRequirementNotDispatched(parentRequirement); // 校验父需求状态是否允许拆分(只能是待分流或实施中) validateParentAllowSplit(parentRequirement); @@ -577,6 +661,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService writeRequirementStatusLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, reason); writeBizAuditLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, buildRequirementFieldChanges(before, requirement), reason); + refreshAncestorStatusRecursively(requirement.getId()); } /** @@ -608,13 +693,21 @@ public class ProductRequirementServiceImpl implements ProductRequirementService permission = PRODUCT_QUERY_PERMISSION) public List getAllowedTransitions(Long requirementId, Long productId) { ProductRequirementDO requirement = validateRequirementExists(requirementId); + // 当产品需求已分流并生成项目需求后,产品需求端不再返回动作按钮 + if (hasDispatchedProjectRequirement(requirement)) { + return Collections.emptyList(); + } + String currentStatus = requirement.getStatusCode(); // 查询当前状态允许的所有流转 List transitions = statusTransitionMapper .selectListByObjectTypeAndFromStatus(REQUIREMENT_OBJECT_TYPE, currentStatus); - return transitions.stream().map(transition -> { + return transitions.stream() + // 取消动作不满足前置条件时,不再返回给前端展示按钮 + .filter(transition -> shouldExposeTransition(requirement, transition)) + .map(transition -> { ProductRequirementStatusTransitionRespVO vo = new ProductRequirementStatusTransitionRespVO(); vo.setActionCode(transition.getActionCode()); vo.setActionName(transition.getActionName()); @@ -629,6 +722,23 @@ public class ProductRequirementServiceImpl implements ProductRequirementService } @Override + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", + permission = PRODUCT_QUERY_PERMISSION) + public boolean hasDispatchedProjectRequirement(Long requirementId, Long productId) { + ProductRequirementDO requirement = validateRequirementExists(requirementId); + return hasDispatchedProjectRequirement(requirement); + } + + /** + * 该方法作用和getAllowedTransitions()类似,是用来获取当前状态下可以进行的动作 + * @deprecated 产品需求页面最开始用来下拉框改状态时使用的,已经弃用 + * + * @param requirementId 需求编号 + * @param productId 产品编号 + * @return ProductRequirementLifecycleRespVO + */ + @Override + @Deprecated @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", permission = PRODUCT_QUERY_PERMISSION) public ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId) { @@ -649,9 +759,176 @@ public class ProductRequirementServiceImpl implements ProductRequirementService lifecycle.setAllowEdit(statusModel.getAllowEdit()); lifecycle.setLastStatusReason(requirement.getLastStatusReason()); lifecycle.setAvailableActions(getAllowedTransitions(requirementId, productId)); + return lifecycle; } + @Override + public ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId) { + // 校验产品需求是否存在,以及是否已分流到具体的实现项目 + ProductRequirementDO requirement = validateRequirementExists(productRequirementId); + if (requirement.getImplementProjectId() == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_NOT_DISPATCHED); + } + Long projectId = requirement.getImplementProjectId(); + + // 查询产品需求分流后生成的顶级项目需求 + List projectRequirements = projectRequirementMapper.selectList( + new LambdaQueryWrapperX() + .eq(ProjectRequirementDO::getProductRequirementId, productRequirementId) + .eq(ProjectRequirementDO::getParentId, 0L) + ); + if (projectRequirements.isEmpty()) { + throw exception(ErrorCodeConstants.REQUIREMENT_DISPATCHED_PROJECT_REQUIREMENT_NOT_FOUND); + } + + // 校验当前登录用户是否为该项目的成员 + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + if (userObjectRoleMapper.selectActiveByObjectAndUserId( + ProjectObjectConstants.OBJECT_TYPE, projectId, loginUserId) == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_NOT_PROJECT_MEMBER); + } + + ProductRequirementDispatchedProjectLinkRespVO respVO = new ProductRequirementDispatchedProjectLinkRespVO(); + respVO.setProjectRequirementId(projectRequirements.get(0).getId()); + respVO.setProjectId(projectId); + return respVO; + } + + /** + * 判断产品需求是否已成功分流并生成对应的项目需求 + */ + public boolean hasDispatchedProjectRequirement(ProductRequirementDO requirement) { + if (requirement.getImplementProjectId() == null) { + return false; + } + List projectRequirements = projectRequirementMapper.selectList( + new LambdaQueryWrapperX() + .eq(ProjectRequirementDO::getSourceType, "product_requirement") + .eq(ProjectRequirementDO::getProductRequirementId, requirement.getId()) + ); + return !projectRequirements.isEmpty(); + } + + /** + * 已分流并生成项目需求后,产品需求端不再允许继续拆分。 + */ + @VisibleForTesting + void validateRequirementNotDispatched(ProductRequirementDO requirement) { + if (hasDispatchedProjectRequirement(requirement)) { + throw exception(ErrorCodeConstants.REQUIREMENT_DISPATCHED_NOT_ALLOW_SPLIT); + } + } + + /** + * 当前只对取消动作做额外过滤,避免前端展示点了也会报错的按钮。 + */ + private boolean shouldExposeTransition(ProductRequirementDO requirement, ObjectStatusTransitionDO transition) { + if (!ACTION_CANCEL.equals(transition.getActionCode())) { + return true; + } + if (!hasChildren(requirement.getId())) { + return true; + } + return isParentCancelAllowed(requirement.getId()); + } + + /** + * 父需求存在子需求时,只有全部子需求都已取消或已拒绝,才允许展示取消动作。 + */ + private boolean isParentCancelAllowed(Long requirementId) { + List allChildren = getAllRequirementsWithChildren(requirementId); + for (ProductRequirementDO req : allChildren) { + if (!Objects.equals(req.getId(), requirementId) + && !CHILD_ALLOW_CANCEL_STATUSES.contains(req.getStatusCode())) { + return false; + } + } + return true; + } + + /** + * 只要存在直接子需求,就应按父需求的取消规则处理。 + */ + private boolean hasChildren(Long requirementId) { + return !requirementMapper.selectListByParentId(requirementId).isEmpty(); + } + + /** + * 子需求状态变化后,仅向上自动推导产品需求父节点状态,不涉及跨对象回写。 + */ + private void refreshAncestorStatusRecursively(Long requirementId) { + ProductRequirementDO currentRequirement = requirementMapper.selectById(requirementId); + if (currentRequirement == null || currentRequirement.getParentId() == null + || Objects.equals(currentRequirement.getParentId(), 0L)) { + return; + } + ProductRequirementDO parentRequirement = requirementMapper.selectById(currentRequirement.getParentId()); + if (parentRequirement == null) { + return; + } + + String targetStatus = deriveParentStatusByDirectChildren(parentRequirement.getId()); + if (!StringUtils.hasText(targetStatus) || Objects.equals(targetStatus, parentRequirement.getStatusCode())) { + return; + } + + ProductRequirementDO before = cloneRequirement(parentRequirement); + int updateCount = requirementMapper.updateStatusByIdAndStatus(parentRequirement.getId(), + parentRequirement.getStatusCode(), targetStatus, AUTO_DERIVE_REASON); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED); + } + parentRequirement.setStatusCode(targetStatus); + parentRequirement.setLastStatusReason(AUTO_DERIVE_REASON); + + writeRequirementStatusLog(parentRequirement, ACTION_AUTO_DERIVE, before.getStatusCode(), + targetStatus, AUTO_DERIVE_REASON); + writeBizAuditLog(parentRequirement, ACTION_AUTO_DERIVE, before.getStatusCode(), + targetStatus, buildRequirementFieldChanges(before, parentRequirement), AUTO_DERIVE_REASON); + + refreshAncestorStatusRecursively(parentRequirement.getId()); + } + + /** + * 父需求状态只在约定好的几种场景下自动变化,其他组合保持当前状态不变: + * accepted 全量汇总为 accepted,closed 全量汇总为 closed, + * cancelled 全量汇总为 cancelled,accepted/closed 混合且至少一个 accepted 时汇总为 accepted。 + */ + private String deriveParentStatusByDirectChildren(Long parentRequirementId) { + List children = requirementMapper.selectListByParentId(parentRequirementId); + if (children.isEmpty()) { + return null; + } + + boolean allAccepted = children.stream() + .allMatch(child -> STATUS_ACCEPTED.equals(child.getStatusCode())); + if (allAccepted) { + return STATUS_ACCEPTED; + } + + boolean allClosed = children.stream() + .allMatch(child -> STATUS_CLOSED.equals(child.getStatusCode())); + if (allClosed) { + return STATUS_CLOSED; + } + + boolean allCancelled = children.stream() + .allMatch(child -> STATUS_CANCELLED.equals(child.getStatusCode())); + if (allCancelled) { + return STATUS_CANCELLED; + } + + boolean allAcceptedOrClosed = children.stream().allMatch(child -> + STATUS_ACCEPTED.equals(child.getStatusCode()) || STATUS_CLOSED.equals(child.getStatusCode())); + boolean hasAccepted = children.stream() + .anyMatch(child -> STATUS_ACCEPTED.equals(child.getStatusCode())); + if (allAcceptedOrClosed && hasAccepted) { + return STATUS_ACCEPTED; + } + return null; + } + // ========== 模块管理 ========== @Override @Transactional(rollbackFor = Exception.class) @@ -758,10 +1035,16 @@ public class ProductRequirementServiceImpl implements ProductRequirementService * 构建需求响应VO(不含子需求) */ private ProductRequirementRespVO buildRequirementRespVO(ProductRequirementDO requirement) { + return buildRequirementRespVO(requirement, getStatusModelMap()); + } + + /** + * 复用已加载的状态模型构建需求响应,避免列表场景重复查状态字典。 + */ + private ProductRequirementRespVO buildRequirementRespVO(ProductRequirementDO requirement, + Map statusModelMap) { ProductRequirementRespVO respVO = BeanUtils.toBean(requirement, ProductRequirementRespVO.class); - // 查询状态名称 - ObjectStatusModelDO statusModel = statusModelMapper - .selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode()); + ObjectStatusModelDO statusModel = statusModelMap.get(requirement.getStatusCode()); if (statusModel != null) { respVO.setStatusName(statusModel.getStatusName()); respVO.setTerminal(statusModel.getTerminalFlag()); @@ -1058,4 +1341,53 @@ public class ProductRequirementServiceImpl implements ProductRequirementService return StringUtils.hasText(value) ? value : ""; } + /** + * dispatch动作且已选择实现项目时,自动将产品需求转化为项目需求 + * + * @param productRequirement 产品需求 + * @param implementProjectId 实现项目ID + */ + @VisibleForTesting + void createProjectRequirementFromProduct(ProductRequirementDO productRequirement, Long implementProjectId) { + List existingRequirements = projectRequirementMapper.selectList( + new LambdaQueryWrapperX() + .eq(ProjectRequirementDO::getSourceType, "product_requirement") + .eq(ProjectRequirementDO::getProductRequirementId, productRequirement.getId()) + ); + if (!existingRequirements.isEmpty()) { + return; + } + + // 查询实现项目下的根模块(parentId = 0) + ProjectRequirementModuleDO rootModule = projectRequirementModuleMapper.selectByProjectIdAndParentId(implementProjectId, 0L); + if (rootModule == null) { + throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_ROOT_NOT_EXISTS); + } + + // 构建项目需求记录 + ProjectRequirementDO newRequirement = new ProjectRequirementDO(); + newRequirement.setParentId(0L); // 顶级需求 + newRequirement.setProjectId(implementProjectId); + newRequirement.setModuleId(rootModule.getId()); + newRequirement.setProductRequirementId(productRequirement.getId()); // 溯源关系 + newRequirement.setSourceType("product_requirement"); + newRequirement.setStatusCode(STATUS_IMPLEMENTING); + newRequirement.setReviewRequired(0); //从产品需求流转到项目需求的需求肯定不需要评审 + // 拷贝产品需求的其他字段(不拷贝排序、状态原因、更新人、更新时间、逻辑删除字段) + newRequirement.setTitle(productRequirement.getTitle()); + newRequirement.setDescription(productRequirement.getDescription()); + newRequirement.setCategory(productRequirement.getCategory()); + newRequirement.setSourceBizId(productRequirement.getSourceBizId()); + newRequirement.setPriority(productRequirement.getPriority()); + newRequirement.setProposerId(productRequirement.getProposerId()); + newRequirement.setProposerNickname(productRequirement.getProposerNickname()); + newRequirement.setCurrentHandlerUserId(productRequirement.getCurrentHandlerUserId()); + newRequirement.setCurrentHandlerUserNickname(productRequirement.getCurrentHandlerUserNickname()); + newRequirement.setWorkHours(productRequirement.getWorkHours()); + newRequirement.setCreator(productRequirement.getCreator()); + newRequirement.setCreateTime(productRequirement.getCreateTime()); + + projectRequirementMapper.insert(newRequirement); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementService.java new file mode 100644 index 0000000..c4ed66f --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementService.java @@ -0,0 +1,109 @@ +package com.njcn.rdms.module.project.service.project; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementLifecycleRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSplitReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusDictRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusTransitionRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementUpdateReqVO; + +import java.util.List; + +/** + * 项目需求 Service 接口 + */ +public interface ProjectRequirementService { + + /** + * 创建项目需求 + */ + Long createRequirement(ProjectRequirementSaveReqVO createReqVO); + + /** + * 更新项目需求 + */ + void updateRequirement(ProjectRequirementUpdateReqVO updateReqVO); + + /** + * 获取需求详情 + */ + ProjectRequirementRespVO getRequirement(Long id, Long projectId); + + /** + * 获取需求分页列表 + */ + PageResult getRequirementPage(ProjectRequirementPageReqVO pageReqVO); + + /** + * 获取需求树形列表 + */ + PageResult getRequirementTree(ProjectRequirementPageReqVO pageReqVO); + + /** + * 变更需求状态 + */ + void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO); + + /** + * 删除需求 + */ + void deleteRequirement(Long id, Long projectId); + + /** + * 拆分需求 + */ + Long splitRequirement(ProjectRequirementSplitReqVO reqVO); + + /** + * 关闭需求 + */ + void closeRequirement(ProjectRequirementCloseReqVO reqVO); + + /** + * 获取需求可执行动作列表 + */ + List getAllowedTransitions(Long requirementId, Long projectId); + + /** + * 获取需求生命周期信息 + */ + ProjectRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long projectId); + + /** + * 创建需求模块 + */ + Long createRequirementModule(ProjectRequirementModuleReqVO reqVO); + + /** + * 更新需求模块 + */ + void updateRequirementModule(ProjectRequirementModuleReqVO reqVO); + + /** + * 删除需求模块 + */ + void deleteRequirementModule(Long moduleId, Long projectId); + + /** + * 获取需求模块树 + */ + List getRequirementModuleTree(Long projectId); + + /** + * 获取需求状态字典 + */ + List getRequirementStatusDict(); + + /** + * 获取需求终态字典 + */ + List getRequirementTerminalStatusDict(); + +}