feat(项目需求): 开发项目需求。

This commit is contained in:
dk
2026-05-13 20:56:48 +08:00
parent 7b4edd6b59
commit 544b56a5d9
26 changed files with 1689 additions and 41 deletions

View File

@@ -81,8 +81,6 @@ public class ProductRequirementController {
@PostMapping("/split")
@Operation(summary = "拆分产品需求")
public CommonResult<Long> 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<Boolean> 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<ProductRequirementDispatchedProjectLinkRespVO> getDispatchedProjectLink(
@RequestParam("productRequirementId") Long productRequirementId) {
return success(requirementService.getDispatchedProjectLink(productRequirementId));
}
// ========== 模块管理 ==========
@PostMapping("/module/create")
@Operation(summary = "创建需求模块")
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {

View File

@@ -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;
}

View File

@@ -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<Long> createRequirement(@Valid @RequestBody ProjectRequirementSaveReqVO createReqVO) {
return success(requirementService.createRequirement(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新项目需求")
public CommonResult<Boolean> 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<ProjectRequirementRespVO> getRequirement(@RequestParam("id") Long id,
@RequestParam("projectId") Long projectId) {
return success(requirementService.getRequirement(id, projectId));
}
@GetMapping("/page")
@Operation(summary = "获取需求分页列表")
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementPage(@Valid ProjectRequirementPageReqVO pageReqVO) {
return success(requirementService.getRequirementPage(pageReqVO));
}
@GetMapping("/tree")
@Operation(summary = "获取需求树形列表")
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementTree(@Valid ProjectRequirementPageReqVO pageReqVO) {
return success(requirementService.getRequirementTree(pageReqVO));
}
@PostMapping("/change-status")
@Operation(summary = "变更需求状态")
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProjectRequirementStatusActionReqVO reqVO) {
requirementService.changeRequirementStatus(reqVO);
return success(true);
}
@PostMapping("/delete")
@Operation(summary = "删除项目需求")
public CommonResult<Boolean> deleteRequirement(@Valid @RequestBody ProjectRequirementDeleteReqVO reqVO) {
requirementService.deleteRequirement(reqVO.getId(), reqVO.getProjectId());
return success(true);
}
@PostMapping("/split")
@Operation(summary = "拆分项目需求")
public CommonResult<Long> splitRequirement(@Valid @RequestBody ProjectRequirementSplitReqVO reqVO) {
return success(requirementService.splitRequirement(reqVO));
}
@PostMapping("/close")
@Operation(summary = "关闭项目需求")
public CommonResult<Boolean> 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<List<ProjectRequirementStatusTransitionRespVO>> 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<ProjectRequirementLifecycleRespVO> getRequirementLifecycle(
@RequestParam("requirementId") Long requirementId,
@RequestParam("projectId") Long projectId) {
return success(requirementService.getRequirementLifecycle(requirementId, projectId));
}
@PostMapping("/module/create")
@Operation(summary = "创建需求模块")
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) {
return success(requirementService.createRequirementModule(reqVO));
}
@PutMapping("/module/update")
@Operation(summary = "更新需求模块")
public CommonResult<Boolean> updateRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) {
requirementService.updateRequirementModule(reqVO);
return success(true);
}
@PostMapping("/module/delete")
@Operation(summary = "删除需求模块")
public CommonResult<Boolean> 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<List<ProjectRequirementModuleRespVO>> getRequirementModuleTree(@RequestParam("projectId") Long projectId) {
return success(requirementService.getRequirementModuleTree(projectId));
}
@GetMapping("/status/dict")
@Operation(summary = "获取需求所有状态字典")
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementStatusDict() {
return success(requirementService.getRequirementStatusDict());
}
@GetMapping("/status/dict/terminal")
@Operation(summary = "获取需求终态状态字典")
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
return success(requirementService.getRequirementTerminalStatusDict());
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<ProjectRequirementStatusTransitionRespVO> availableActions;
}

View File

@@ -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;
}

View File

@@ -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 = "父模块 ID0 表示顶级", 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;
}

View File

@@ -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 = "父模块 ID0 表示顶级", 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<ProjectRequirementModuleRespVO> children;
}

View File

@@ -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<Long> 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;
}

View File

@@ -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 = "父需求 ID0 表示顶级需求", 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 = "<p>详细描述需求内容</p>")
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<ProjectRequirementRespVO> children;
@Schema(description = "是否为终态", example = "false")
private Boolean terminal;
}

View File

@@ -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 = "<p>详细描述需求内容</p>")
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;
}

View File

@@ -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 = "<p>详细描述需求内容</p>")
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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 = "<p>详细描述需求内容</p>")
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;
}

View File

@@ -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;
/**
* 父需求 ID0 表示顶级需求
*/
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;
}

View File

@@ -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;
/**
* 父模块 ID0 表示顶级模块
*/
private Long parentId;
/**
* 所属项目 ID
*/
private Long projectId;
/**
* 模块名称
*/
private String moduleName;
/**
* 模块说明
*/
private String remark;
/**
* 图标
*/
private String icon;
/**
* 排序值
*/
private Integer sort;
}

View File

@@ -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;
}

View File

@@ -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<ProjectRequirementDO> {
/**
* 分页查询需求列表
*/
default PageResult<ProjectRequirementDO> selectPage(ProjectRequirementPageReqVO reqVO) {
return selectPage(reqVO, buildQueryWrapper(reqVO));
}
/**
* 查询所有符合条件的需求列表,用于树查询
*/
default List<ProjectRequirementDO> selectList(ProjectRequirementPageReqVO reqVO) {
return selectList(buildQueryWrapper(reqVO));
}
/**
* 构建查询条件
*/
private LambdaQueryWrapperX<ProjectRequirementDO> buildQueryWrapper(ProjectRequirementPageReqVO reqVO) {
LambdaQueryWrapperX<ProjectRequirementDO> 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<ProjectRequirementDO> selectListByParentId(Long parentId) {
return selectList(new LambdaQueryWrapperX<ProjectRequirementDO>()
.eq(ProjectRequirementDO::getParentId, parentId)
.orderByAsc(ProjectRequirementDO::getSort)
.orderByDesc(ProjectRequirementDO::getCreateTime));
}
/**
* 根据模块 ID 查询需求列表
*/
default List<ProjectRequirementDO> selectListByModuleId(Long moduleId) {
return selectList(new LambdaQueryWrapperX<ProjectRequirementDO>()
.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<ProjectRequirementDO>()
.eq(ProjectRequirementDO::getId, id)
.eq(ProjectRequirementDO::getStatusCode, fromStatus));
}
/**
* 根据 ID 和状态删除,带并发控制
*/
default int deleteByIdAndStatus(Long id, String statusCode) {
return delete(new LambdaQueryWrapperX<ProjectRequirementDO>()
.eq(ProjectRequirementDO::getId, id)
.eq(ProjectRequirementDO::getStatusCode, statusCode));
}
}

View File

@@ -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<ProjectRequirementModuleDO> {
/**
* 根据项目 ID 查询模块列表
*/
default List<ProjectRequirementModuleDO> selectListByProjectId(Long projectId) {
return selectList(new LambdaQueryWrapperX<ProjectRequirementModuleDO>()
.eq(ProjectRequirementModuleDO::getProjectId, projectId)
.orderByAsc(ProjectRequirementModuleDO::getSort)
.orderByAsc(ProjectRequirementModuleDO::getCreateTime));
}
/**
* 根据父模块 ID 查询子模块列表
*/
default List<ProjectRequirementModuleDO> selectListByParentId(Long parentId) {
return selectList(new LambdaQueryWrapperX<ProjectRequirementModuleDO>()
.eq(ProjectRequirementModuleDO::getParentId, parentId)
.orderByAsc(ProjectRequirementModuleDO::getSort));
}
/**
* 根据项目 ID 和模块名称查询模块
*/
default ProjectRequirementModuleDO selectByProjectIdAndModuleName(Long projectId, String moduleName) {
return selectOne(new LambdaQueryWrapperX<ProjectRequirementModuleDO>()
.eq(ProjectRequirementModuleDO::getProjectId, projectId)
.eq(ProjectRequirementModuleDO::getModuleName, moduleName));
}
/**
* 根据项目 ID 和父模块 ID 查询模块
*/
default ProjectRequirementModuleDO selectByProjectIdAndParentId(Long projectId, Long parentId) {
return selectOne(new LambdaQueryWrapperX<ProjectRequirementModuleDO>()
.eq(ProjectRequirementModuleDO::getProjectId, projectId)
.eq(ProjectRequirementModuleDO::getParentId, parentId));
}
}

View File

@@ -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<ProjectRequirementStatusLogDO> {
/**
* 根据需求 ID 查询状态变更日志列表
*/
default List<ProjectRequirementStatusLogDO> selectListByRequirementId(Long requirementId) {
return selectList(new LambdaQueryWrapperX<ProjectRequirementStatusLogDO>()
.eq(ProjectRequirementStatusLogDO::getRequirementId, requirementId)
.orderByDesc(ProjectRequirementStatusLogDO::getCreateTime));
}
}

View File

@@ -88,6 +88,15 @@ public interface ProductRequirementService {
*/
List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId);
/**
* 判断需求是否已分流并生成项目需求
*
* @param requirementId 需求编号
* @param productId 产品编号
* @return 是否已分流
*/
boolean hasDispatchedProjectRequirement(Long requirementId, Long productId);
/**
* 获取需求生命周期信息(当前状态 + 可执行动作)
*
@@ -144,4 +153,12 @@ public interface ProductRequirementService {
*/
List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
/**
* 获取产品需求分流后对应的项目需求跳转链接
*
* @param productRequirementId 产品需求编号
* @return 项目需求ID和实现项目ID
*/
ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId);
}

View File

@@ -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<ProductRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO);
Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();
List<ProductRequirementRespVO> 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<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();
// 第二步找出所有匹配需求的根节点ID同时收集路径上的所有节点ID
Set<Long> rootIds = new HashSet<>();
Set<Long> pathNodeIds = new HashSet<>();
Map<Long, ProductRequirementDO> requirementCache = new HashMap<>();
Map<Long, ProductRequirementDO> 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<ProductRequirementDO> pagedRootRequirements = rootRequirements.subList(fromIndex, toIndex);
Map<Long, List<ProductRequirementDO>> childrenMap = buildPathChildrenMap(pathNodeIds);
// 第五步:构建树形结构(只包含路径上的节点)
List<ProductRequirementRespVO> 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 根节点IDparentId = 0L 的需求ID
*/
private Long findRootRequirementIdAndCollectPath(ProductRequirementDO requirement,
Map<Long, ProductRequirementDO> cache,
Set<Long> 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<Long, ProductRequirementDO> cache,
Set<Long> 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<Long> pathNodeIds) {
ProductRequirementRespVO respVO = buildRequirementRespVO(requirement);
// 查询子需求
List<ProductRequirementDO> allChildren = requirementMapper.selectListByParentId(requirement.getId());
// 只保留路径上的子需求
Set<Long> pathNodeIds,
Map<Long, List<ProductRequirementDO>> childrenMap,
Map<String, ObjectStatusModelDO> statusModelMap) {
ProductRequirementRespVO respVO = buildRequirementRespVO(requirement, statusModelMap);
List<ProductRequirementDO> allChildren = childrenMap.getOrDefault(requirement.getId(), Collections.emptyList());
List<ProductRequirementDO> 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<Long, ProductRequirementDO> buildRequirementCacheWithAncestors(List<ProductRequirementDO> matchedRequirements) {
Map<Long, ProductRequirementDO> cache = matchedRequirements.stream()
.collect(Collectors.toMap(ProductRequirementDO::getId, Function.identity(), (left, right) -> left, HashMap::new));
Set<Long> pendingParentIds = matchedRequirements.stream()
.map(ProductRequirementDO::getParentId)
.filter(parentId -> parentId != null && parentId != 0L && !cache.containsKey(parentId))
.collect(Collectors.toSet());
while (!pendingParentIds.isEmpty()) {
List<ProductRequirementDO> parentRequirements = requirementMapper.selectBatchIds(pendingParentIds);
if (parentRequirements.isEmpty()) {
break;
}
Set<Long> 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<Long, List<ProductRequirementDO>> buildPathChildrenMap(Set<Long> pathNodeIds) {
if (pathNodeIds.isEmpty()) {
return Collections.emptyMap();
}
List<ProductRequirementDO> pathChildren = requirementMapper.selectList(
new LambdaQueryWrapperX<ProductRequirementDO>()
.in(ProductRequirementDO::getParentId, pathNodeIds)
.orderByAsc(ProductRequirementDO::getSort)
.orderByDesc(ProductRequirementDO::getCreateTime));
Map<Long, List<ProductRequirementDO>> childrenMap = new HashMap<>();
for (ProductRequirementDO child : pathChildren) {
childrenMap.computeIfAbsent(child.getParentId(), key -> new ArrayList<>()).add(child);
}
return childrenMap;
}
/**
* 一次性加载当前对象类型下的状态模型,供列表和树查询复用。
*/
private Map<String, ObjectStatusModelDO> 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<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId) {
ProductRequirementDO requirement = validateRequirementExists(requirementId);
// 当产品需求已分流并生成项目需求后,产品需求端不再返回动作按钮
if (hasDispatchedProjectRequirement(requirement)) {
return Collections.emptyList();
}
String currentStatus = requirement.getStatusCode();
// 查询当前状态允许的所有流转
List<ObjectStatusTransitionDO> 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<ProjectRequirementDO> projectRequirements = projectRequirementMapper.selectList(
new LambdaQueryWrapperX<ProjectRequirementDO>()
.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<ProjectRequirementDO> projectRequirements = projectRequirementMapper.selectList(
new LambdaQueryWrapperX<ProjectRequirementDO>()
.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<ProductRequirementDO> 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 全量汇总为 acceptedclosed 全量汇总为 closed
* cancelled 全量汇总为 cancelledaccepted/closed 混合且至少一个 accepted 时汇总为 accepted。
*/
private String deriveParentStatusByDirectChildren(Long parentRequirementId) {
List<ProductRequirementDO> 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<String, ObjectStatusModelDO> 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<ProjectRequirementDO> existingRequirements = projectRequirementMapper.selectList(
new LambdaQueryWrapperX<ProjectRequirementDO>()
.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);
}
}

View File

@@ -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<ProjectRequirementRespVO> getRequirementPage(ProjectRequirementPageReqVO pageReqVO);
/**
* 获取需求树形列表
*/
PageResult<ProjectRequirementRespVO> getRequirementTree(ProjectRequirementPageReqVO pageReqVO);
/**
* 变更需求状态
*/
void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO);
/**
* 删除需求
*/
void deleteRequirement(Long id, Long projectId);
/**
* 拆分需求
*/
Long splitRequirement(ProjectRequirementSplitReqVO reqVO);
/**
* 关闭需求
*/
void closeRequirement(ProjectRequirementCloseReqVO reqVO);
/**
* 获取需求可执行动作列表
*/
List<ProjectRequirementStatusTransitionRespVO> 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<ProjectRequirementModuleRespVO> getRequirementModuleTree(Long projectId);
/**
* 获取需求状态字典
*/
List<ProjectRequirementStatusDictRespVO> getRequirementStatusDict();
/**
* 获取需求终态字典
*/
List<ProjectRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
}