Compare commits

...

5 Commits

35 changed files with 3155 additions and 52 deletions

View File

@@ -4,7 +4,7 @@ import com.njcn.rdms.framework.common.exception.ErrorCode;
/**
* Project 错误码枚举类
*
* <p>
* 产品管理当前使用 1-008-001-000 段。
*/
public interface ErrorCodeConstants {
@@ -57,6 +57,11 @@ public interface ErrorCodeConstants {
ErrorCode REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS = new ErrorCode(1_008_002_012, "模块下存在非终态需求,不可删除");
ErrorCode REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_002_015, "存在子模块,请先删除子模块");
ErrorCode REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_002_016, "模块下存在需求,请先删除需求");
ErrorCode PROJECT_REQUIREMENT_MODULE_ROOT_NOT_EXISTS = new ErrorCode(1_008_002_018, "实现项目下不存在根模块,请先创建项目模块");
ErrorCode REQUIREMENT_DISPATCHED_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_019, "产品需求已分流生成项目需求,不允许再在产品端拆分");
ErrorCode REQUIREMENT_NOT_DISPATCHED = new ErrorCode(1_008_002_020, "该产品需求尚未分流到实现项目");
ErrorCode REQUIREMENT_DISPATCHED_PROJECT_REQUIREMENT_NOT_FOUND = new ErrorCode(1_008_002_021, "未找到该产品需求对应的项目需求");
ErrorCode REQUIREMENT_NOT_PROJECT_MEMBER = new ErrorCode(1_008_002_022, "您不是该项目的成员,无权访问");
// ========== 项目管理 1-008-002-000 ==========
ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1_008_002_000, "项目不存在");
@@ -162,4 +167,22 @@ public interface ErrorCodeConstants {
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_NOT_ALLOWED = new ErrorCode(1_008_007_004, "附件扩展名【{}】不在允许列表内");
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_BLOCKED = new ErrorCode(1_008_007_005, "附件类型【{}】被禁止上传");
// ========== 项目需求 1_008_007_xxx ==========
ErrorCode PROJECT_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "项目需求不存在");
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_007_001, "当前项目需求状态不支持动作【{}】");
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_007_002, "动作【{}】必须填写原因");
ErrorCode PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_007_003, "项目需求状态已发生变化,请刷新后重试");
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_007_004, "当前项目需求状态为终态,不允许编辑");
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_005, "只有已验收的项目需求才能关闭");
ErrorCode PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_007_006, "项目需求状态定义不存在或已停用");
ErrorCode PROJECT_REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_007_007, "父需求状态不是实施中,不允许拆分");
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_008, "存在未处理完的子需求,请先处理子需求");
ErrorCode PROJECT_REQUIREMENT_MODULE_NOT_EXISTS = new ErrorCode(1_008_007_009, "项目需求模块不存在");
ErrorCode PROJECT_REQUIREMENT_MODULE_NAME_DUPLICATE = new ErrorCode(1_008_007_010, "已经存在名称为【{}】的项目需求模块");
ErrorCode PROJECT_REQUIREMENT_MODULE_NOT_BELONG_TO_PROJECT = new ErrorCode(1_008_007_011, "模块不属于当前项目");
ErrorCode PROJECT_REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_007_013, "存在子需求,请先删除子需求");
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_007_014, "只有待确认、待评审状态的项目需求才能删除");
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_007_015, "存在子模块,请先删除子模块");
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_007_016, "模块下存在项目需求,请先删除需求");
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_017, "只有不存在子需求,或子需求都处于已取消和已拒绝状态时,父需求才允许取消");
}

View File

@@ -55,6 +55,11 @@ public final class ProjectObjectConstants {
*/
public static final String PERMISSION_STATUS = "project:project:status";
/**
* 项目拆分权限码。
*/
public static final String PERMISSION_SPLIT = "project:project:split";
/**
* 项目删除权限码。
*/

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

@@ -1,5 +1,6 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -37,13 +38,13 @@ public class ProductRequirementRespVO {
@Schema(description = "需求分类名称", example = "功能需求")
private String categoryName;
@Schema(description = "需求来源类型manual 表示手工新增work_order 表示工单流转", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual")
@Schema(description = "需求来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual")
private String sourceType;
@Schema(description = "来源业务ID", example = "1024")
private Long sourceBizId;
@Schema(description = "优先级0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@Schema(description = "优先级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer priority;
@Schema(description = "优先级名称", example = "")
@@ -88,10 +89,13 @@ public class ProductRequirementRespVO {
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
@Schema(description = "附件列表")
private List<AttachmentItem> attachments;
@Schema(description = "子需求列表,树形结构")
private List<ProductRequirementRespVO> children;
@Schema(description = "是否为终态,已拒绝、已取消、已关闭都算终态", example = "false")
@Schema(description = "是否为终态", example = "false")
private Boolean terminal;
}

View File

@@ -1,11 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 管理后台 - 产品需求保存 Request VO
*/
@@ -29,7 +33,7 @@ public class ProductRequirementSaveReqVO {
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
@NotBlank(message = "需求标题不能为空")
@Size(max = 200, message = "需求标题长度不能超过 200 个字符")
@Size(max = 200, message = "需求标题长度不能超过200个字符")
private String title;
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
@@ -37,7 +41,7 @@ public class ProductRequirementSaveReqVO {
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
@NotBlank(message = "需求分类不能为空")
@Size(max = 64, message = "需求分类长度不能超过 64 个字符")
@Size(max = 64, message = "需求分类长度不能超过64个字符")
private String category;
@Schema(description = "优先级0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@@ -67,4 +71,8 @@ public class ProductRequirementSaveReqVO {
@Schema(description = "排序值,越小越靠前", example = "0")
private Integer sort;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

View File

@@ -1,11 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 管理后台 - 产品需求拆分 Request VO
*/
@@ -30,7 +34,7 @@ public class ProductRequirementSplitReqVO {
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
@NotBlank(message = "需求标题不能为空")
@Size(max = 200, message = "需求标题长度不能超过 200 个字符")
@Size(max = 200, message = "需求标题长度不能超过200个字符")
private String title;
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
@@ -38,7 +42,7 @@ public class ProductRequirementSplitReqVO {
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
@NotBlank(message = "需求分类不能为空")
@Size(max = 64, message = "需求分类长度不能超过 64 个字符")
@Size(max = 64, message = "需求分类长度不能超过64个字符")
private String category;
@Schema(description = "优先级0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@@ -68,4 +72,8 @@ public class ProductRequirementSplitReqVO {
@Schema(description = "排序值,越小越靠前", example = "0")
private Integer sort;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

View File

@@ -1,11 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 管理后台 - 产品需求编辑 Request VO
*/
@@ -30,7 +34,7 @@ public class ProductRequirementUpdateReqVO {
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
@NotBlank(message = "需求标题不能为空")
@Size(max = 200, message = "需求标题长度不能超过 200 个字符")
@Size(max = 200, message = "需求标题长度不能超过200个字符")
private String title;
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
@@ -38,7 +42,7 @@ public class ProductRequirementUpdateReqVO {
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
@NotBlank(message = "需求分类不能为空")
@Size(max = 64, message = "需求分类长度不能超过 64 个字符")
@Size(max = 64, message = "需求分类长度不能超过64个字符")
private String category;
@Schema(description = "优先级0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@@ -68,4 +72,8 @@ public class ProductRequirementUpdateReqVO {
@Schema(description = "排序值,越小越靠前", example = "0")
private Integer sort;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

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,98 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
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<AttachmentItem> attachments;
@Schema(description = "子需求列表,树形结构")
private List<ProjectRequirementRespVO> children;
@Schema(description = "是否为终态", example = "false")
private Boolean terminal;
}

View File

@@ -0,0 +1,75 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 管理后台 - 项目需求保存 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;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

View File

@@ -0,0 +1,76 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 管理后台 - 项目需求拆分 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;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

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,76 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 管理后台 - 项目需求编辑 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;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

View File

@@ -1,15 +1,20 @@
package com.njcn.rdms.module.project.dal.dataobject.product;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 产品需求主表
*/
@TableName("rdms_product_requirement")
@TableName(value = "rdms_product_requirement", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
public class ProductRequirementDO extends BaseDO {
@@ -95,5 +100,10 @@ public class ProductRequirementDO extends BaseDO {
* 排序值,越小越靠前
*/
private Integer sort;
/**
* 闄勪欢鍒楄〃锛圝SON锛夈€傚厓绱?{@link AttachmentItem}锛歩d / url / name / size / contentType銆?
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<AttachmentItem> attachments;
}

View File

@@ -0,0 +1,109 @@
package com.njcn.rdms.module.project.dal.dataobject.project;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 项目需求主表
*/
@TableName(value = "rdms_project_requirement", autoResultMap = true)
@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;
/**
* 闄勪欢鍒楄〃锛圝SON锛夈€傚厓绱?{@link AttachmentItem}锛歩d / url / name / size / contentType銆?
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<AttachmentItem> attachments;
}

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,21 +4,30 @@ 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;
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -75,7 +84,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 +104,14 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
private ObjectStatusTransitionMapper statusTransitionMapper;
@Resource
private ObjectStatusModelMapper statusModelMapper;
@Resource
private ProjectRequirementMapper projectRequirementMapper;
@Resource
private ProjectRequirementModuleMapper projectRequirementModuleMapper;
@Resource
private UserObjectRoleMapper userObjectRoleMapper;
@Resource
private AttachmentFileIdResolver attachmentFileIdResolver;
// ========== 需求增删改查 ==========
@@ -101,6 +124,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
Long moduleId = resolveModuleId(createReqVO.getModuleId(), createReqVO.getProductId());
// 校验模块是否属于当前产品
validateModuleBelongsToProduct(moduleId, createReqVO.getProductId());
AttachmentValidator.validate(createReqVO.getAttachments());
attachmentFileIdResolver.resolve(createReqVO.getAttachments());
ProductRequirementDO requirement = new ProductRequirementDO();
requirement.setProductId(createReqVO.getProductId());
@@ -123,6 +148,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
requirement.setCurrentHandlerUserNickname(normalizeNullableText(createReqVO.getCurrentHandlerUserNickname()));
requirement.setImplementProjectId(createReqVO.getImplementProjectId());
requirement.setSort(createReqVO.getSort() != null ? createReqVO.getSort() : 0);
requirement.setAttachments(createReqVO.getAttachments());
requirementMapper.insert(requirement);
// 写入业务审计日志
@@ -143,6 +169,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProductId());
// 校验模块是否属于当前产品
validateModuleBelongsToProduct(moduleId, updateReqVO.getProductId());
AttachmentValidator.validate(updateReqVO.getAttachments());
attachmentFileIdResolver.resolve(updateReqVO.getAttachments());
ProductRequirementDO before = cloneRequirement(requirement);
String fromStatus = requirement.getStatusCode();
@@ -159,6 +187,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
requirement.setCurrentHandlerUserNickname(normalizeNullableText(updateReqVO.getCurrentHandlerUserNickname()));
requirement.setImplementProjectId(updateReqVO.getImplementProjectId());
requirement.setSort(updateReqVO.getSort() != null ? updateReqVO.getSort() : 0);
requirement.setAttachments(updateReqVO.getAttachments());
requirementMapper.updateById(requirement);
writeBizAuditLog(requirement, ACTION_UPDATE, fromStatus, requirement.getStatusCode(),
@@ -190,8 +219,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 +247,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 +281,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 +293,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 +411,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
/**
* 递归获取模块及其所有子模块的ID列表
* @param moduleId 起始模块ID
*
* @param moduleId 起始模块ID
* @param productId 产品ID
* @return 包含自身及所有子模块的ID列表
*/
@@ -355,6 +441,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
/**
* 递归获取需求及其所有子需求(包含子子需求)
*
* @param requirementId 起始需求ID
* @return 包含自身及所有后代需求的列表
*/
@@ -398,17 +485,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 +516,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
// 写入业务审计日志
writeBizAuditLog(requirement, actionCode, fromStatus, toStatus,
buildRequirementFieldChanges(before, requirement), reason);
refreshAncestorStatusRecursively(requirement.getId());
}
/**
@@ -496,8 +588,12 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
public Long splitRequirement(ProductRequirementSplitReqVO reqVO) {
// 校验父需求是否存在
ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId());
// 产品需求一旦已分流生成项目需求,就只能到项目需求侧继续拆分
validateRequirementNotDispatched(parentRequirement);
// 校验父需求状态是否允许拆分(只能是待分流或实施中)
validateParentAllowSplit(parentRequirement);
AttachmentValidator.validate(reqVO.getAttachments());
attachmentFileIdResolver.resolve(reqVO.getAttachments());
// 创建子需求
ProductRequirementDO childRequirement = new ProductRequirementDO();
@@ -522,6 +618,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
childRequirement.setCurrentHandlerUserNickname(normalizeNullableText(reqVO.getCurrentHandlerUserNickname()));
childRequirement.setImplementProjectId(reqVO.getImplementProjectId());
childRequirement.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
childRequirement.setAttachments(reqVO.getAttachments());
requirementMapper.insert(childRequirement);
// 父需求状态从待分流变为实施中
@@ -577,6 +674,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 +706,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 +735,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 +772,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 +1048,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());
@@ -974,6 +1270,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
target.setCurrentHandlerUserNickname(source.getCurrentHandlerUserNickname());
target.setImplementProjectId(source.getImplementProjectId());
target.setSort(source.getSort());
target.setAttachments(source.getAttachments());
return target;
}
@@ -1021,6 +1318,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
valueOf(after, ProductRequirementDO::getImplementProjectId));
appendFieldChange(fieldChanges, "sort", valueOf(before, ProductRequirementDO::getSort),
valueOf(after, ProductRequirementDO::getSort));
appendFieldChange(fieldChanges, "attachments", valueOf(before, ProductRequirementDO::getAttachments),
valueOf(after, ProductRequirementDO::getAttachments));
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
}
@@ -1058,4 +1357,54 @@ 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.setAttachments(productRequirement.getAttachments());
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();
}

View File

@@ -25,6 +25,7 @@ import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementModuleDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
@@ -32,6 +33,7 @@ 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.ProductMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
@@ -98,6 +100,8 @@ class ProjectServiceImpl implements ProjectService {
private AdminUserApi adminUserApi;
@Resource
private DictDataApi dictDataApi;
@Resource
private ProjectRequirementModuleMapper projectRequirementModuleMapper;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -128,6 +132,7 @@ class ProjectServiceImpl implements ProjectService {
projectMapper.insert(project);
initManagerMemberRelation(project);
initDefaultRequirementModule(project);
writeBizAuditLog(project, ObjectActivityConstants.PROJECT_ACTION_CREATE, null, initialStatus,
buildProjectFieldChanges(null, project), null);
return project.getId();
@@ -404,6 +409,19 @@ class ProjectServiceImpl implements ProjectService {
return product == null ? null : product.getName();
}
/**
* 项目创建后自动初始化“全部需求”根模块,作为该项目需求树的唯一根节点。
*/
private void initDefaultRequirementModule(ProjectDO project) {
ProjectRequirementModuleDO module = new ProjectRequirementModuleDO();
module.setParentId(0L);
module.setProjectId(project.getId());
module.setModuleName("全部需求");
module.setRemark("自动创建的模块");
module.setSort(0);
projectRequirementModuleMapper.insert(module);
}
private String getManagerNickname(Long managerUserId) {
if (managerUserId == null) {
return null;