feat(新增需求评审功能): 新增需求评审功能。
fix(产品需求、项目需求): 按照会议意见修改诸多细节。 fix(产品对象域的概览界面): 提供相应接口给前端。
This commit is contained in:
@@ -51,11 +51,11 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_001, "当前需求状态不支持动作【{}】");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_002, "动作【{}】必须填写原因");
|
||||
ErrorCode REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_003, "需求状态已发生变化,请刷新后重试");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_004, "当前需求状态为终态,不允许编辑");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_004, "当前需求状态不允许编辑");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_005, "只有已验收的需求才能关闭");
|
||||
ErrorCode REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_006, "需求状态定义不存在或已停用");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待确认、待评审、待分流状态的需求才能删除");
|
||||
ErrorCode REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_007, "父需求状态不是待分流或实施中,不允许拆分");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待认领、待评审、待指派状态的需求才能删除");
|
||||
ErrorCode REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_007, "父需求状态不是已评审、待指派、实施中,不允许拆分");
|
||||
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_008, "存在未处理完的子需求,请先处理子需求");
|
||||
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_002_017, "只有不存在子需求,或子需求都处于已取消和已拒绝的状态才能取消");
|
||||
ErrorCode REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_002_013, "存在子需求,请先删除子需求");
|
||||
@@ -65,12 +65,15 @@ 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_PROJECT_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_HANDLER_NOT_PRODUCT_MEMBER = new ErrorCode(1_008_002_023, "当前需求负责人不是此产品团队成员,请重新选择");
|
||||
ErrorCode REQUIREMENT_REVIEW_ALREADY_EXISTS = new ErrorCode(1_008_002_024, "该产品需求已提交评审记录");
|
||||
ErrorCode REQUIREMENT_REVIEW_NOT_EXISTS = new ErrorCode(1_008_002_025, "产品需求评审记录不存在");
|
||||
ErrorCode REQUIREMENT_REVIEW_CONCLUSION_INVALID = new ErrorCode(1_008_002_026, "产品需求评审结论不合法");
|
||||
ErrorCode REQUIREMENT_NOT_PROJECT_MEMBER = new ErrorCode(1_008_002_022, "您不是该项目的成员,无权访问");
|
||||
ErrorCode REQUIREMENT_HANDLER_NOT_PROJECT_MEMBER = new ErrorCode(1_008_002_023, "当前需求负责人不是所选分流项目的成员,请重新选择");
|
||||
|
||||
// ========== 项目管理 1-008-002-000 ==========
|
||||
ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1_008_002_000, "项目不存在");
|
||||
@@ -195,21 +198,24 @@ public interface ErrorCodeConstants {
|
||||
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_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_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_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, "只有不存在子需求,或子需求都处于已取消和已拒绝状态时,父需求才允许取消");
|
||||
ErrorCode PROJECT_REQUIREMENT_HAS_EXECUTIONS_NOT_ALLOW_DELETE = new ErrorCode(1_008_007_018, "该项目需求下存在承接执行,请先解绑或转移");
|
||||
ErrorCode PROJECT_REQUIREMENT_SYNCED_FROM_PRODUCT_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_019, "\u7531\u4ea7\u54c1\u9700\u6c42\u6d41\u8f6c\u751f\u6210\u7684\u9879\u76ee\u9700\u6c42\u4e0d\u5141\u8bb8\u53d6\u6d88");
|
||||
ErrorCode PROJECT_REQUIREMENT_SYNCED_FROM_PRODUCT_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_019, "从产品侧流转来的需求不可取消");
|
||||
ErrorCode PROJECT_REQUIREMENT_REVIEW_ALREADY_EXISTS = new ErrorCode(1_008_007_020, "该项目需求已提交评审记录");
|
||||
ErrorCode PROJECT_REQUIREMENT_REVIEW_NOT_EXISTS = new ErrorCode(1_008_007_021, "项目需求评审记录不存在");
|
||||
ErrorCode PROJECT_REQUIREMENT_REVIEW_CONCLUSION_INVALID = new ErrorCode(1_008_007_022, "项目需求评审结论不合法");
|
||||
|
||||
// ========== 个人事项 1_008_008_xxx ==========
|
||||
ErrorCode PERSONAL_ITEM_NOT_EXISTS = new ErrorCode(1_008_008_001, "个人事项不存在");
|
||||
|
||||
@@ -38,6 +38,11 @@ public final class ProductObjectConstants {
|
||||
*/
|
||||
public static final String PERMISSION_STATUS = "project:product:status";
|
||||
|
||||
/**
|
||||
* 产品需求评审权限码。
|
||||
*/
|
||||
public static final String PERMISSION_REVIEW = "project:product:review";
|
||||
|
||||
/**
|
||||
* 产品删除权限码。
|
||||
*/
|
||||
|
||||
@@ -55,6 +55,11 @@ public final class ProjectObjectConstants {
|
||||
*/
|
||||
public static final String PERMISSION_STATUS = "project:project:status";
|
||||
|
||||
/**
|
||||
* 项目需求评审权限码。
|
||||
*/
|
||||
public static final String PERMISSION_REVIEW = "project:project:review";
|
||||
|
||||
/**
|
||||
* 项目拆分权限码。
|
||||
*/
|
||||
|
||||
@@ -28,8 +28,6 @@ public class ProductRequirementController {
|
||||
@Resource
|
||||
private ProductRequirementService requirementService;
|
||||
|
||||
// ========== 需求管理 ==========
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建产品需求")
|
||||
public CommonResult<Long> createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) {
|
||||
@@ -59,11 +57,18 @@ public class ProductRequirementController {
|
||||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
@Operation(summary = "获取需求树形列表(分页)")
|
||||
@Operation(summary = "获取需求树分页列表")
|
||||
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementTree(@Valid ProductRequirementPageReqVO pageReqVO) {
|
||||
return success(requirementService.getRequirementTree(pageReqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
@Operation(summary = "获取产品需求概览数据")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<ProductRequirementDashboardRespVO> getRequirementDashboard(@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getRequirementDashboard(productId));
|
||||
}
|
||||
|
||||
@PostMapping("/change-status")
|
||||
@Operation(summary = "变更需求状态")
|
||||
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) {
|
||||
@@ -125,16 +130,6 @@ public class ProductRequirementController {
|
||||
return success(requirementService.hasDispatchedProjectRequirementBatch(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/lifecycle")
|
||||
@Operation(summary = "获取需求生命周期信息")
|
||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<ProductRequirementLifecycleRespVO> getRequirementLifecycle(
|
||||
@RequestParam("requirementId") Long requirementId,
|
||||
@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getRequirementLifecycle(requirementId, productId));
|
||||
}
|
||||
|
||||
@GetMapping("/dispatched-project-link")
|
||||
@Operation(summary = "获取产品需求分流后对应的项目需求跳转链接")
|
||||
@Parameter(name = "productRequirementId", description = "产品需求编号", required = true, example = "1024")
|
||||
@@ -143,7 +138,6 @@ public class ProductRequirementController {
|
||||
return success(requirementService.getDispatchedProjectLink(productRequirementId));
|
||||
}
|
||||
|
||||
// ========== 模块管理 ==========
|
||||
@PostMapping("/module/create")
|
||||
@Operation(summary = "创建需求模块")
|
||||
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求概览最近变化 Response VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求概览最近变化 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementDashboardRecentChangeRespVO {
|
||||
|
||||
@Schema(description = "前端列表唯一键", requiredMode = Schema.RequiredMode.REQUIRED, example = "requirement:create:2048")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "产品需求ID", example = "1003")
|
||||
private Long requirementId;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "补充对象首页需求池统计接口")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "动作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "create")
|
||||
private String actionType;
|
||||
|
||||
@Schema(description = "动作名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "需求新增")
|
||||
private String actionLabel;
|
||||
|
||||
@Schema(description = "展示内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "当前状态:待评审")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "发生时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime occurredAt;
|
||||
|
||||
@Schema(description = "操作人用户ID", example = "1024")
|
||||
private Long operatorUserId;
|
||||
|
||||
@Schema(description = "操作人名称快照", example = "张三")
|
||||
private String operatorName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.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 ProductRequirementDashboardRespVO {
|
||||
|
||||
@Schema(description = "需求池统计")
|
||||
private ProductRequirementDashboardSummaryRespVO summary;
|
||||
|
||||
@Schema(description = "需求池最近重要变化")
|
||||
private List<ProductRequirementDashboardRecentChangeRespVO> recentChanges;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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 ProductRequirementDashboardSummaryRespVO {
|
||||
|
||||
@Schema(description = "需求总量,包括根需求和子需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "待处理需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
private Long todo;
|
||||
|
||||
@Schema(description = "待认领需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long pendingClaim;
|
||||
|
||||
@Schema(description = "待评审需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long pendingReview;
|
||||
|
||||
@Schema(description = "待指派需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long pendingDispatch;
|
||||
|
||||
@Schema(description = "完成需求数,已验收和已关闭计入完成", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
|
||||
private Long completed;
|
||||
|
||||
@Schema(description = "完成率,四舍五入后的百分比整数", requiredMode = Schema.RequiredMode.REQUIRED, example = "33")
|
||||
private Integer completionRate;
|
||||
|
||||
@Schema(description = "高优先待处理需求数,P0/P1 且处于待处理状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
private Long highPriorityTodo;
|
||||
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求分流后项目需求跳转链接 Response VO
|
||||
* 管理后台 - 产品需求指派后项目需求跳转链接 Response VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求分流后项目需求跳转链接 Response VO")
|
||||
@Schema(description = "管理后台 - 产品需求指派后项目需求跳转链接 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementDispatchedProjectLinkRespVO {
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品需求批量分流状态 Response VO")
|
||||
@Schema(description = "管理后台 - 产品需求批量指派状态 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementHasDispatchedBatchRespVO {
|
||||
|
||||
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long requirementId;
|
||||
|
||||
@Schema(description = "是否已分流生成项目需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@Schema(description = "是否已指派生成项目需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
private Boolean hasDispatched;
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.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 ProductRequirementLifecycleRespVO {
|
||||
|
||||
@Schema(description = "当前状态编码", example = "pending_dispatch")
|
||||
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<ProductRequirementStatusTransitionRespVO> availableActions;
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -54,10 +55,10 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "当前状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending_dispatch")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "当前状态名称", example = "待分流")
|
||||
@Schema(description = "当前状态名称", example = "待指派")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
||||
@Schema(description = "最近一次状态动作原因", example = "需求全部结束")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@@ -95,8 +96,4 @@ public class ProductRequirementRespVO {
|
||||
|
||||
@Schema(description = "子需求列表,树形结构")
|
||||
private List<ProductRequirementRespVO> children;
|
||||
|
||||
@Schema(description = "是否为终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class ProductRequirementStatusActionReqVO {
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "状态变更原因", example = "评审通过,进入分流阶段")
|
||||
@Schema(description = "状态变更原因", example = "需求全部完成")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "关联项目编号(dispatch动作时可选)", example = "1024")
|
||||
|
||||
@@ -10,10 +10,10 @@ import lombok.Data;
|
||||
@Data
|
||||
public class ProductRequirementStatusDictRespVO {
|
||||
|
||||
@Schema(description = "状态编码", example = "pending_confirm")
|
||||
@Schema(description = "状态编码", example = "pending_claim")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "状态名称", example = "待确认")
|
||||
@Schema(description = "状态名称", example = "待认领")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "排序值", example = "1")
|
||||
@@ -25,4 +25,7 @@ public class ProductRequirementStatusDictRespVO {
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminalFlag;
|
||||
|
||||
@Schema(description = "是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ProductRequirementStatusTransitionRespVO {
|
||||
@Schema(description = "动作编码", example = "dispatch")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "明确分流/拆分")
|
||||
@Schema(description = "动作名称", example = "明确指派/拆分")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "目标状态编码", example = "implementing")
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.Proj
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
||||
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;
|
||||
@@ -72,7 +71,7 @@ public class ProjectRequirementController {
|
||||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
@Operation(summary = "获取需求树形列表")
|
||||
@Operation(summary = "获取需求树分页列表")
|
||||
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementTree(@Valid ProjectRequirementPageReqVO pageReqVO) {
|
||||
return success(requirementService.getRequirementTree(pageReqVO));
|
||||
}
|
||||
@@ -121,16 +120,6 @@ public class ProjectRequirementController {
|
||||
return success(requirementService.getAllowedTransitionsBatch(reqVO));
|
||||
}
|
||||
|
||||
@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) {
|
||||
@@ -165,7 +154,7 @@ public class ProjectRequirementController {
|
||||
}
|
||||
|
||||
@GetMapping("/status/dict/terminal")
|
||||
@Operation(summary = "获取需求终态状态字典")
|
||||
@Operation(summary = "获取需求终止态状态字典")
|
||||
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
|
||||
return success(requirementService.getRequirementTerminalStatusDict());
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
|
||||
}
|
||||
@@ -61,7 +61,7 @@ public class ProjectRequirementRespVO {
|
||||
@Schema(description = "当前状态名称", example = "实施中")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
||||
@Schema(description = "最近一次状态动作原因", example = "需求全部结束")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "提出人用户ID", example = "1024")
|
||||
|
||||
@@ -26,7 +26,7 @@ public class ProjectRequirementStatusActionReqVO {
|
||||
@Size(max = 32, message = "动作编码长度不能超过 32 个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "状态变更原因", example = "评审通过")
|
||||
@Schema(description = "状态变更原因", example = "需求全部结束")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import lombok.Data;
|
||||
@Data
|
||||
public class ProjectRequirementStatusDictRespVO {
|
||||
|
||||
@Schema(description = "状态编码", example = "pending_confirm")
|
||||
@Schema(description = "状态编码", example = "pending_claim")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "状态名称", example = "待确认")
|
||||
@Schema(description = "状态名称", example = "待认领")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "排序值", example = "1")
|
||||
@@ -25,4 +25,7 @@ public class ProjectRequirementStatusDictRespVO {
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminalFlag;
|
||||
|
||||
@Schema(description = "是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ProjectRequirementStatusTransitionRespVO {
|
||||
@Schema(description = "动作编码", example = "pass_review")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "评审通过")
|
||||
@Schema(description = "动作名称", example = "通过评审")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "目标状态编码", example = "implementing")
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.review;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.review.vo.RequirementReviewRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.review.vo.RequirementReviewSubmitReqVO;
|
||||
import com.njcn.rdms.module.project.service.review.RequirementReviewService;
|
||||
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.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 需求评审控制器
|
||||
*/
|
||||
@Tag(name = "管理后台 - 需求评审")
|
||||
@RestController
|
||||
@Validated
|
||||
public class RequirementReviewController {
|
||||
|
||||
@Resource
|
||||
private RequirementReviewService reviewService;
|
||||
|
||||
@PostMapping("/project/product/requirement/review/submit")
|
||||
@Operation(summary = "提交产品需求评审")
|
||||
public CommonResult<Long> submitProductRequirementReview(@Valid @RequestBody RequirementReviewSubmitReqVO reqVO) {
|
||||
return success(reviewService.submitProductRequirementReview(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/project/product/requirement/review/get")
|
||||
@Operation(summary = "获取产品需求评审记录")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "4096")
|
||||
public CommonResult<RequirementReviewRespVO> getProductRequirementReview(@RequestParam("productId") Long productId,
|
||||
@RequestParam("requirementId") Long requirementId) {
|
||||
return success(reviewService.getProductRequirementReview(productId, requirementId));
|
||||
}
|
||||
|
||||
@PostMapping("/project/project/requirement/review/submit")
|
||||
@Operation(summary = "提交项目需求评审")
|
||||
public CommonResult<Long> submitProjectRequirementReview(@Valid @RequestBody RequirementReviewSubmitReqVO reqVO) {
|
||||
return success(reviewService.submitProjectRequirementReview(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/project/project/requirement/review/get")
|
||||
@Operation(summary = "获取项目需求评审记录")
|
||||
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
|
||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "4096")
|
||||
public CommonResult<RequirementReviewRespVO> getProjectRequirementReview(@RequestParam("projectId") Long projectId,
|
||||
@RequestParam("requirementId") Long requirementId) {
|
||||
return success(reviewService.getProjectRequirementReview(projectId, requirementId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.review.vo;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.review.RequirementReviewAttendeeItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 需求评审 Response VO")
|
||||
@Data
|
||||
public class RequirementReviewRespVO {
|
||||
|
||||
@Schema(description = "评审记录编号", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "对象类型", example = "product_requirement")
|
||||
private String objectType;
|
||||
|
||||
@Schema(description = "需求编号", example = "4096")
|
||||
private Long requirementId;
|
||||
|
||||
@Schema(description = "评审操作人编号", example = "1001")
|
||||
private Long operatorId;
|
||||
|
||||
@Schema(description = "评审结论:0 通过,1 不通过", example = "1")
|
||||
private Integer conclusion;
|
||||
|
||||
@Schema(description = "评审内容,支持富文本")
|
||||
private String reviewContent;
|
||||
|
||||
@Schema(description = "需求预估工时", example = "16.5")
|
||||
private BigDecimal requirementEstimatedHours;
|
||||
|
||||
@Schema(description = "参会人列表")
|
||||
private List<RequirementReviewAttendeeItem> attendees;
|
||||
|
||||
@Schema(description = "会议资料附件")
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
@Schema(description = "实际评审日期", example = "2026-05-20")
|
||||
private LocalDate reviewTime;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.review.vo;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.review.RequirementReviewAttendeeItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 需求评审提交 Request VO")
|
||||
@Data
|
||||
public class RequirementReviewSubmitReqVO {
|
||||
|
||||
@Schema(description = "产品编号,产品需求评审时必填", example = "1024")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "项目编号,项目需求评审时必填", example = "2048")
|
||||
private Long projectId;
|
||||
|
||||
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
|
||||
@NotNull(message = "需求编号不能为空")
|
||||
private Long requirementId;
|
||||
|
||||
@Schema(description = "评审操作人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||
@NotNull(message = "评审操作人不能为空")
|
||||
private Long operatorId;
|
||||
|
||||
@Schema(description = "评审结论:0 通过,1 不通过", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "评审结论不能为空")
|
||||
@Min(value = 0, message = "评审结论不合法")
|
||||
@Max(value = 1, message = "评审结论不合法")
|
||||
private Integer conclusion;
|
||||
|
||||
@Schema(description = "评审内容,支持富文本")
|
||||
private String reviewContent;
|
||||
|
||||
@Schema(description = "需求预估工时", example = "16.5")
|
||||
private BigDecimal requirementEstimatedHours;
|
||||
|
||||
@Schema(description = "参会人列表")
|
||||
private List<RequirementReviewAttendeeItem> attendees;
|
||||
|
||||
@Schema(description = "会议资料附件")
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
@Schema(description = "实际评审日期", example = "2026-05-20")
|
||||
private LocalDate reviewTime;
|
||||
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public class ProductRequirementDO extends BaseDO {
|
||||
*/
|
||||
private String currentHandlerUserNickname;
|
||||
/**
|
||||
* 默认实现项目ID,分流后可回填
|
||||
* 默认实现项目ID,指派后可回填
|
||||
*/
|
||||
private Long implementProjectId;
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
@@ -51,9 +52,4 @@ public class ProductRequirementStatusLogDO extends BaseDO {
|
||||
* 需求标题快照
|
||||
*/
|
||||
private String requirementTitleSnapshot;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.review;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 需求评审参会人快照。
|
||||
*/
|
||||
@Schema(description = "需求评审参会人快照")
|
||||
@Data
|
||||
public class RequirementReviewAttendeeItem {
|
||||
|
||||
@Schema(description = "用户编号", example = "1024")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户昵称", example = "张三")
|
||||
private String nickname;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.review;
|
||||
|
||||
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.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 需求评审记录。
|
||||
*/
|
||||
@TableName(value = "rdms_requirement_review", autoResultMap = true)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class RequirementReviewDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private String objectType;
|
||||
|
||||
private Long requirementId;
|
||||
|
||||
private Long operatorId;
|
||||
|
||||
/**
|
||||
* 评审结论:1 通过,2 不通过。
|
||||
*/
|
||||
private Integer conclusion;
|
||||
|
||||
private String reviewContent;
|
||||
|
||||
private BigDecimal requirementEstimatedHours;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<RequirementReviewAttendeeItem> attendees;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
private LocalDateTime reviewTime;
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -22,4 +23,20 @@ public interface ProductRequirementStatusLogMapper extends BaseMapperX<ProductRe
|
||||
.orderByDesc(ProductRequirementStatusLogDO::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据需求ID集合和目标状态集合查询状态变更日志。
|
||||
*/
|
||||
default List<ProductRequirementStatusLogDO> selectListByRequirementIdsAndToStatuses(List<Long> requirementIds,
|
||||
List<String> toStatuses) {
|
||||
if (requirementIds == null || requirementIds.isEmpty()
|
||||
|| toStatuses == null || toStatuses.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementStatusLogDO>()
|
||||
.in(ProductRequirementStatusLogDO::getRequirementId, requirementIds)
|
||||
.in(ProductRequirementStatusLogDO::getToStatus, toStatuses)
|
||||
.orderByDesc(ProductRequirementStatusLogDO::getCreateTime)
|
||||
.orderByDesc(ProductRequirementStatusLogDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.review;
|
||||
|
||||
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.review.RequirementReviewDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RequirementReviewMapper extends BaseMapperX<RequirementReviewDO> {
|
||||
|
||||
default RequirementReviewDO selectByObjectTypeAndRequirementId(String objectType, Long requirementId) {
|
||||
return selectOne(new LambdaQueryWrapperX<RequirementReviewDO>()
|
||||
.eq(RequirementReviewDO::getObjectType, objectType)
|
||||
.eq(RequirementReviewDO::getRequirementId, requirementId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,171 +10,48 @@ import java.util.List;
|
||||
*/
|
||||
public interface ProductRequirementService {
|
||||
|
||||
/**
|
||||
* 创建产品需求
|
||||
*
|
||||
* @param createReqVO 创建请求
|
||||
* @return 需求编号
|
||||
*/
|
||||
Long createRequirement(ProductRequirementSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新产品需求(不含状态变更)
|
||||
*
|
||||
* @param updateReqVO 更新请求
|
||||
*/
|
||||
void updateRequirement(ProductRequirementUpdateReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 获取需求详情
|
||||
*
|
||||
* @param id 需求编号
|
||||
* @return 需求详情
|
||||
*/
|
||||
ProductRequirementRespVO getRequirement(Long id, Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求分页列表
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获取需求树形列表(分页)
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页结果(只按父需求分页,子需求不计入分页)
|
||||
*/
|
||||
PageResult<ProductRequirementRespVO> getRequirementTree(ProductRequirementPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 变更需求状态
|
||||
*
|
||||
* @param reqVO 状态动作请求
|
||||
*/
|
||||
ProductRequirementDashboardRespVO getRequirementDashboard(Long productId);
|
||||
|
||||
void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除需求
|
||||
*
|
||||
* @param id 需求编号
|
||||
* @param productId 产品编号
|
||||
*/
|
||||
void changeRequirementStatusForReview(ProductRequirementStatusActionReqVO reqVO);
|
||||
|
||||
void deleteRequirement(Long id, Long productId);
|
||||
|
||||
/**
|
||||
* 拆分需求(创建子需求)
|
||||
*
|
||||
* @param reqVO 拆分请求
|
||||
* @return 子需求编号
|
||||
*/
|
||||
Long splitRequirement(ProductRequirementSplitReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 关闭需求(大需求关闭时级联关闭子需求)
|
||||
*
|
||||
* @param reqVO 关闭请求
|
||||
*/
|
||||
void closeRequirement(ProductRequirementCloseReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 获取需求当前可执行的状态动作列表
|
||||
*
|
||||
* @param requirementId 需求编号
|
||||
* @param productId 产品编号
|
||||
* @return 可执行动作列表
|
||||
*/
|
||||
List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId);
|
||||
|
||||
/**
|
||||
* 批量获取需求当前可执行的状态动作列表
|
||||
*
|
||||
* @param reqVO 批量查询请求
|
||||
* @return 按需求编号标识的可执行动作列表
|
||||
*/
|
||||
List<ProductRequirementAllowedTransitionBatchRespVO> getAllowedTransitionsBatch(ProductRequirementBatchReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 判断需求是否已分流并生成项目需求
|
||||
*
|
||||
* @param requirementId 需求编号
|
||||
* @param productId 产品编号
|
||||
* @return 是否已分流
|
||||
*/
|
||||
boolean hasDispatchedProjectRequirement(Long requirementId, Long productId);
|
||||
|
||||
/**
|
||||
* 批量判断需求是否已分流并生成项目需求
|
||||
*
|
||||
* @param reqVO 批量查询请求
|
||||
* @return 按需求编号标识的分流状态
|
||||
*/
|
||||
List<ProductRequirementHasDispatchedBatchRespVO> hasDispatchedProjectRequirementBatch(ProductRequirementBatchReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 获取需求生命周期信息(当前状态 + 可执行动作)
|
||||
*
|
||||
* @param requirementId 需求编号
|
||||
* @param productId 产品编号
|
||||
* @return 生命周期信息
|
||||
*/
|
||||
ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId);
|
||||
|
||||
// ========== 模块管理 ==========
|
||||
|
||||
/**
|
||||
* 创建需求模块
|
||||
*
|
||||
* @param reqVO 模块保存请求
|
||||
* @return 模块编号
|
||||
*/
|
||||
Long createRequirementModule(ProductRequirementModuleReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 更新需求模块
|
||||
*
|
||||
* @param reqVO 模块保存请求
|
||||
*/
|
||||
void updateRequirementModule(ProductRequirementModuleReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除需求模块(级联删除模块下需求)
|
||||
*
|
||||
* @param moduleId 模块编号
|
||||
* @param productId 产品编号
|
||||
*/
|
||||
void deleteRequirementModule(Long moduleId, Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求模块树
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 模块树
|
||||
*/
|
||||
List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求所有状态字典列表
|
||||
*
|
||||
* @return 状态字典列表
|
||||
*/
|
||||
List<ProductRequirementStatusDictRespVO> getRequirementStatusDict();
|
||||
|
||||
/**
|
||||
* 获取需求终止态状态字典列表
|
||||
*
|
||||
* @return 终止态状态字典列表
|
||||
*/
|
||||
List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
|
||||
|
||||
/**
|
||||
* 获取产品需求分流后对应的项目需求跳转链接
|
||||
*
|
||||
* @param productRequirementId 产品需求编号
|
||||
* @return 项目需求ID和关联项目ID
|
||||
*/
|
||||
ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
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.constant.ProductObjectConstants;
|
||||
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;
|
||||
@@ -35,6 +37,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -53,21 +56,21 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
private static final String PRODUCT_OBJECT_TYPE = "product";
|
||||
|
||||
// 需求状态常量
|
||||
private static final String STATUS_PENDING_CONFIRM = "pending_confirm";
|
||||
private static final String STATUS_PENDING_CLAIM = "pending_claim";
|
||||
private static final String STATUS_PENDING_REVIEW = "pending_review";
|
||||
private static final String STATUS_PENDING_DISPATCH = "pending_dispatch";
|
||||
private static final String STATUS_REVIEWED = "reviewed";
|
||||
private static final String STATUS_IMPLEMENTING = "implementing";
|
||||
private static final String STATUS_ACCEPTED = "accepted";
|
||||
private static final String STATUS_CLOSED = "closed";
|
||||
private static final String STATUS_REJECTED = "rejected";
|
||||
private static final String STATUS_CANCELLED = "cancelled";
|
||||
private static final String STATUS_REVIEW_REJECTED = "review_rejected";
|
||||
private static final String SOURCE_TYPE_MANUAL = "manual";
|
||||
private static final String SOURCE_TYPE_WORK_ORDER = "work_order";
|
||||
|
||||
// 终态状态集合
|
||||
private static final List<String> TERMINAL_STATUSES = List.of(STATUS_CLOSED, STATUS_REJECTED, STATUS_CANCELLED);
|
||||
// 子需求允许大需求关闭的状态集合
|
||||
private static final List<String> CHILD_ALLOW_CLOSE_STATUSES = List.of(STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED);
|
||||
// 允许删除的状态集合(实施中之前的状态)
|
||||
private static final List<String> ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH);
|
||||
private static final List<String> ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CLAIM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH);
|
||||
// 父需求取消时,子需求允许的状态集合(仅已拒绝和已取消)
|
||||
private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(STATUS_REJECTED, STATUS_CANCELLED);
|
||||
|
||||
@@ -76,6 +79,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
private static final String PRODUCT_QUERY_PERMISSION = "project:product:query";
|
||||
private static final String PRODUCT_UPDATE_PERMISSION = "project:product:update";
|
||||
private static final String PRODUCT_STATUS_PERMISSION = "project:product:status";
|
||||
private static final String PRODUCT_REVIEW_PERMISSION = ProductObjectConstants.PERMISSION_REVIEW;
|
||||
private static final String PRODUCT_DELETE_PERMISSION = "project:product:delete";
|
||||
private static final String PRODUCT_SPLIT_PERMISSION = "project:product:split";
|
||||
|
||||
@@ -88,10 +92,18 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
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_REJECT = "reject";
|
||||
private static final String ACTION_PASS_REVIEW = "pass_review";
|
||||
private static final String ACTION_REJECT_REVIEW = "reject_review";
|
||||
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 = "根据子需求状态自动推导";
|
||||
private static final int DASHBOARD_RECENT_CHANGE_LIMIT = 5;
|
||||
private static final String DASHBOARD_ACTION_STATUS_TERMINAL = "status_terminal";
|
||||
private static final String DASHBOARD_LABEL_CREATE = "需求新增";
|
||||
private static final String DASHBOARD_LABEL_DELETE = "需求删除";
|
||||
private static final String DASHBOARD_LABEL_STATUS = "状态流转";
|
||||
|
||||
@Resource
|
||||
private ProductRequirementMapper requirementMapper;
|
||||
@@ -136,7 +148,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
requirement.setTitle(createReqVO.getTitle().trim());
|
||||
requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
|
||||
requirement.setCategory(createReqVO.getCategory());
|
||||
requirement.setSourceType("manual"); // 手工新增默认来源类型
|
||||
requirement.setSourceType(SOURCE_TYPE_MANUAL); // 手工新增默认来源类型
|
||||
requirement.setPriority(createReqVO.getPriority());
|
||||
// 根据是否需要评审确定初始状态
|
||||
String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
|
||||
@@ -164,7 +176,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
permission = PRODUCT_UPDATE_PERMISSION)
|
||||
public void updateRequirement(ProductRequirementUpdateReqVO updateReqVO) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
|
||||
// 校验终态不允许编辑
|
||||
// 校验当前状态是否允许编辑
|
||||
validateRequirementEditable(requirement);
|
||||
// 当未选择模块时,自动归属到该产品的"全部需求"模块
|
||||
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProductId());
|
||||
@@ -292,6 +304,263 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
return new PageResult<>(list, (long) total);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public ProductRequirementDashboardRespVO getRequirementDashboard(Long productId) {
|
||||
List<ProductRequirementDO> requirements = requirementMapper.selectListByProductId(productId);
|
||||
Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();
|
||||
|
||||
ProductRequirementDashboardRespVO respVO = new ProductRequirementDashboardRespVO();
|
||||
respVO.setSummary(buildRequirementDashboardSummary(requirements));
|
||||
respVO.setRecentChanges(buildRequirementDashboardRecentChanges(productId, requirements, statusModelMap));
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private ProductRequirementDashboardSummaryRespVO buildRequirementDashboardSummary(List<ProductRequirementDO> requirements) {
|
||||
long total = requirements.size();
|
||||
long pendingClaim = requirements.stream()
|
||||
.filter(requirement -> STATUS_PENDING_CLAIM.equals(requirement.getStatusCode()))
|
||||
.count();
|
||||
long pendingReview = requirements.stream()
|
||||
.filter(requirement -> STATUS_PENDING_REVIEW.equals(requirement.getStatusCode()))
|
||||
.count();
|
||||
long pendingDispatch = requirements.stream()
|
||||
.filter(requirement -> isPendingDispatchActionStatus(requirement.getStatusCode()))
|
||||
.count();
|
||||
long todo = pendingClaim + pendingReview + pendingDispatch;
|
||||
long completed = requirements.stream()
|
||||
.filter(requirement -> STATUS_ACCEPTED.equals(requirement.getStatusCode())
|
||||
|| STATUS_CLOSED.equals(requirement.getStatusCode()))
|
||||
.count();
|
||||
long highPriorityTodo = requirements.stream()
|
||||
.filter(requirement -> isTodoStatus(requirement.getStatusCode()))
|
||||
.filter(requirement -> requirement.getPriority() != null
|
||||
&& (requirement.getPriority() == 0 || requirement.getPriority() == 1))
|
||||
.count();
|
||||
|
||||
ProductRequirementDashboardSummaryRespVO summary = new ProductRequirementDashboardSummaryRespVO();
|
||||
summary.setTotal(total);
|
||||
summary.setTodo(todo);
|
||||
summary.setPendingClaim(pendingClaim);
|
||||
summary.setPendingReview(pendingReview);
|
||||
summary.setPendingDispatch(pendingDispatch);
|
||||
summary.setCompleted(completed);
|
||||
summary.setCompletionRate(total == 0 ? 0 : Math.toIntExact(Math.round(completed * 100.0 / total)));
|
||||
summary.setHighPriorityTodo(highPriorityTodo);
|
||||
return summary;
|
||||
}
|
||||
|
||||
private List<ProductRequirementDashboardRecentChangeRespVO> buildRequirementDashboardRecentChanges(
|
||||
Long productId, List<ProductRequirementDO> requirements, Map<String, ObjectStatusModelDO> statusModelMap) {
|
||||
List<DashboardRecentChangeItem> items = new ArrayList<>();
|
||||
appendRequirementAuditRecentChanges(productId, statusModelMap, items);
|
||||
appendRequirementTerminalStatusRecentChanges(requirements, statusModelMap, items);
|
||||
|
||||
items.sort((left, right) -> {
|
||||
int timeCompare = compareNullableLocalDateTimeDesc(left.occurredAt(), right.occurredAt());
|
||||
if (timeCompare != 0) {
|
||||
return timeCompare;
|
||||
}
|
||||
return compareNullableLongDesc(left.sourceId(), right.sourceId());
|
||||
});
|
||||
|
||||
return items.stream()
|
||||
.limit(DASHBOARD_RECENT_CHANGE_LIMIT)
|
||||
.map(DashboardRecentChangeItem::respVO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private int compareNullableLocalDateTimeDesc(LocalDateTime left, LocalDateTime right) {
|
||||
if (left == null && right == null) {
|
||||
return 0;
|
||||
}
|
||||
if (left == null) {
|
||||
return 1;
|
||||
}
|
||||
if (right == null) {
|
||||
return -1;
|
||||
}
|
||||
return right.compareTo(left);
|
||||
}
|
||||
|
||||
private int compareNullableLongDesc(Long left, Long right) {
|
||||
if (left == null && right == null) {
|
||||
return 0;
|
||||
}
|
||||
if (left == null) {
|
||||
return 1;
|
||||
}
|
||||
if (right == null) {
|
||||
return -1;
|
||||
}
|
||||
return right.compareTo(left);
|
||||
}
|
||||
|
||||
private void appendRequirementAuditRecentChanges(Long productId, Map<String, ObjectStatusModelDO> statusModelMap,
|
||||
List<DashboardRecentChangeItem> items) {
|
||||
List<BizAuditLogDO> logs = bizAuditLogMapper.selectListByBizTypeAndActions(
|
||||
BIZ_TYPE_REQUIREMENT, List.of(ACTION_CREATE, ACTION_DELETE), null, null);
|
||||
for (BizAuditLogDO log : logs) {
|
||||
if (ACTION_CREATE.equals(log.getActionType())) {
|
||||
appendRequirementCreateRecentChange(productId, statusModelMap, items, log);
|
||||
} else if (ACTION_DELETE.equals(log.getActionType())) {
|
||||
appendRequirementDeleteRecentChange(productId, items, log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendRequirementCreateRecentChange(Long productId, Map<String, ObjectStatusModelDO> statusModelMap,
|
||||
List<DashboardRecentChangeItem> items, BizAuditLogDO log) {
|
||||
Long logProductId = getFieldChangeLong(log.getFieldChanges(), "productId", "after");
|
||||
if (!Objects.equals(logProductId, productId)) {
|
||||
return;
|
||||
}
|
||||
String title = getFieldChangeString(log.getFieldChanges(), "title", "after");
|
||||
String statusCode = getFieldChangeString(log.getFieldChanges(), "statusCode", "after");
|
||||
ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
|
||||
"requirement:create:" + log.getId(),
|
||||
log.getBizId(),
|
||||
defaultDashboardTitle(title),
|
||||
ACTION_CREATE,
|
||||
DASHBOARD_LABEL_CREATE,
|
||||
"当前状态:" + resolveStatusName(statusModelMap, statusCode),
|
||||
log.getCreateTime(),
|
||||
log.getOperatorUserId(),
|
||||
log.getOperatorName());
|
||||
items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
|
||||
}
|
||||
|
||||
private void appendRequirementDeleteRecentChange(Long productId, List<DashboardRecentChangeItem> items,
|
||||
BizAuditLogDO log) {
|
||||
Long logProductId = getFieldChangeLong(log.getFieldChanges(), "productId", "before");
|
||||
if (!Objects.equals(logProductId, productId)) {
|
||||
return;
|
||||
}
|
||||
String title = getFieldChangeString(log.getFieldChanges(), "title", "before");
|
||||
ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
|
||||
"requirement:delete:" + log.getId(),
|
||||
log.getBizId(),
|
||||
defaultDashboardTitle(title),
|
||||
ACTION_DELETE,
|
||||
DASHBOARD_LABEL_DELETE,
|
||||
"该需求已被删除",
|
||||
log.getCreateTime(),
|
||||
log.getOperatorUserId(),
|
||||
log.getOperatorName());
|
||||
items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
|
||||
}
|
||||
|
||||
private void appendRequirementTerminalStatusRecentChanges(List<ProductRequirementDO> requirements,
|
||||
Map<String, ObjectStatusModelDO> statusModelMap,
|
||||
List<DashboardRecentChangeItem> items) {
|
||||
List<Long> requirementIds = requirements.stream()
|
||||
.map(ProductRequirementDO::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
List<String> terminalStatusCodes = statusModelMapper
|
||||
.selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE);
|
||||
List<ProductRequirementStatusLogDO> logs = statusLogMapper
|
||||
.selectListByRequirementIdsAndToStatuses(requirementIds, terminalStatusCodes);
|
||||
for (ProductRequirementStatusLogDO log : logs) {
|
||||
String statusName = resolveStatusName(statusModelMap, log.getToStatus());
|
||||
ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
|
||||
"requirement:status:" + log.getId(),
|
||||
log.getRequirementId(),
|
||||
defaultDashboardTitle(log.getRequirementTitleSnapshot()),
|
||||
DASHBOARD_ACTION_STATUS_TERMINAL,
|
||||
DASHBOARD_LABEL_STATUS,
|
||||
"流转至终止态:" + statusName,
|
||||
log.getCreateTime(),
|
||||
log.getOperatorUserId(),
|
||||
log.getOperatorName());
|
||||
items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
|
||||
}
|
||||
}
|
||||
|
||||
private ProductRequirementDashboardRecentChangeRespVO buildDashboardRecentChange(
|
||||
String id, Long requirementId, String title, String actionType, String actionLabel,
|
||||
String content, LocalDateTime occurredAt, Long operatorUserId, String operatorName) {
|
||||
ProductRequirementDashboardRecentChangeRespVO respVO = new ProductRequirementDashboardRecentChangeRespVO();
|
||||
respVO.setId(id);
|
||||
respVO.setRequirementId(requirementId);
|
||||
respVO.setTitle(title);
|
||||
respVO.setActionType(actionType);
|
||||
respVO.setActionLabel(actionLabel);
|
||||
respVO.setContent(content);
|
||||
respVO.setOccurredAt(occurredAt);
|
||||
respVO.setOperatorUserId(operatorUserId);
|
||||
respVO.setOperatorName(operatorName);
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private String resolveStatusName(Map<String, ObjectStatusModelDO> statusModelMap, String statusCode) {
|
||||
if (!StringUtils.hasText(statusCode)) {
|
||||
return "未知状态";
|
||||
}
|
||||
ObjectStatusModelDO statusModel = statusModelMap.get(statusCode);
|
||||
return statusModel != null && StringUtils.hasText(statusModel.getStatusName())
|
||||
? statusModel.getStatusName() : statusCode;
|
||||
}
|
||||
|
||||
private String defaultDashboardTitle(String title) {
|
||||
return StringUtils.hasText(title) ? title : "未命名需求";
|
||||
}
|
||||
|
||||
private boolean isTodoStatus(String statusCode) {
|
||||
return STATUS_PENDING_CLAIM.equals(statusCode)
|
||||
|| STATUS_PENDING_REVIEW.equals(statusCode)
|
||||
|| isPendingDispatchActionStatus(statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品对象域的概览首页“待指派”表示等待执行指派动作,不等同于单一 pending_dispatch 状态。
|
||||
*/
|
||||
private boolean isPendingDispatchActionStatus(String statusCode) {
|
||||
return STATUS_PENDING_DISPATCH.equals(statusCode) || STATUS_REVIEWED.equals(statusCode);
|
||||
}
|
||||
|
||||
private Long getFieldChangeLong(String fieldChanges, String fieldName, String valueField) {
|
||||
JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
|
||||
if (valueNode == null || valueNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (valueNode.isNumber()) {
|
||||
return valueNode.longValue();
|
||||
}
|
||||
if (valueNode.isTextual() && StringUtils.hasText(valueNode.textValue())) {
|
||||
return Long.valueOf(valueNode.textValue().trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getFieldChangeString(String fieldChanges, String fieldName, String valueField) {
|
||||
JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
|
||||
if (valueNode == null || valueNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (valueNode.isTextual()) {
|
||||
return valueNode.textValue();
|
||||
}
|
||||
return valueNode.asText();
|
||||
}
|
||||
|
||||
private JsonNode getFieldChangeNode(String fieldChanges, String fieldName, String valueField) {
|
||||
if (!StringUtils.hasText(fieldChanges) || !JsonUtils.isJsonObject(fieldChanges)) {
|
||||
return null;
|
||||
}
|
||||
JsonNode fieldNode = JsonUtils.parseTree(fieldChanges).path(fieldName);
|
||||
if (fieldNode.isMissingNode()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode valueNode = fieldNode.path(valueField);
|
||||
return valueNode.isMissingNode() ? null : valueNode;
|
||||
}
|
||||
|
||||
private record DashboardRecentChangeItem(Long sourceId, LocalDateTime occurredAt,
|
||||
ProductRequirementDashboardRecentChangeRespVO respVO) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上追溯需求的根节点ID,同时收集路径上的所有节点ID
|
||||
*
|
||||
@@ -471,6 +740,19 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_STATUS_PERMISSION)
|
||||
public void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
|
||||
doChangeRequirementStatus(reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_REVIEW_PERMISSION)
|
||||
public void changeRequirementStatusForReview(ProductRequirementStatusActionReqVO reqVO) {
|
||||
validateReviewStatusAction(reqVO.getActionCode());
|
||||
doChangeRequirementStatus(reqVO);
|
||||
}
|
||||
|
||||
private void doChangeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
||||
String actionCode = reqVO.getActionCode().trim();
|
||||
Long implementProjectId = reqVO.getImplementProjectId();
|
||||
@@ -479,6 +761,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
|
||||
// 校验状态流转是否合法
|
||||
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
||||
validateReviewRejectedActionAllowed(requirement, actionCode);
|
||||
String reason = normalizeNullableText(reqVO.getReason());
|
||||
// 校验是否需要填写原因
|
||||
validateTransitionReason(transition, reason);
|
||||
@@ -497,14 +780,14 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
if (ACTION_CLOSE.equals(actionCode)) {
|
||||
closeAllAcceptedChildren(reqVO.getId(), reason);
|
||||
}
|
||||
// dispatch动作且选择了关联项目时,校验负责人是否在项目中,然后自动创建对应的项目需求
|
||||
// dispatch动作且选择了关联项目时,校验负责人是否在产品团队中,然后自动创建对应的项目需求
|
||||
if (ACTION_DISPATCH.equals(actionCode) && implementProjectId != null) {
|
||||
// 校验负责人是否为目标项目的成员
|
||||
// 校验负责人是否为所属产品的团队成员
|
||||
if (requirement.getCurrentHandlerUserId() != null) {
|
||||
List<UserObjectRoleDO> userObjectRoleDOS = userObjectRoleMapper.selectActiveListByObjectAndUserId(
|
||||
ProjectObjectConstants.OBJECT_TYPE, implementProjectId, requirement.getCurrentHandlerUserId());
|
||||
ProductObjectConstants.OBJECT_TYPE, requirement.getProductId(), requirement.getCurrentHandlerUserId());
|
||||
if (userObjectRoleDOS.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_HANDLER_NOT_PROJECT_MEMBER);
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_HANDLER_NOT_PRODUCT_MEMBER);
|
||||
}
|
||||
}
|
||||
createProjectRequirementFromProduct(requirement, implementProjectId);
|
||||
@@ -528,16 +811,24 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
refreshAncestorStatusRecursively(requirement.getId());
|
||||
}
|
||||
|
||||
private void validateReviewStatusAction(String actionCode) {
|
||||
String normalizedActionCode = actionCode == null ? null : actionCode.trim();
|
||||
if (!ACTION_PASS_REVIEW.equals(normalizedActionCode) && !ACTION_REJECT_REVIEW.equals(normalizedActionCode)) {
|
||||
throw invalidParamException("评审权限只能触发评审通过或评审不通过动作");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验需求的所有子需求(包括子子需求)是否处于允许关闭或验收的状态
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
|
||||
List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
||||
Set<String> terminalStatusCodes = getTerminalStatusCodes();
|
||||
// 排除自身,只校验子需求
|
||||
for (ProductRequirementDO req : allChildren) {
|
||||
if (!Objects.equals(req.getId(), requirementId)) {
|
||||
if (!CHILD_ALLOW_CLOSE_STATUSES.contains(req.getStatusCode())) {
|
||||
if (!STATUS_ACCEPTED.equals(req.getStatusCode()) && !terminalStatusCodes.contains(req.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
|
||||
}
|
||||
}
|
||||
@@ -575,7 +866,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_HAS_CHILDREN);
|
||||
}
|
||||
|
||||
// 校验状态是否允许删除(只有待确认、待评审、待分流状态才能删除)
|
||||
// 校验状态是否允许删除(只有待认领、待评审、待指派状态才能删除)
|
||||
if (!ALLOW_DELETE_STATUSES.contains(fromStatus)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_DELETE);
|
||||
}
|
||||
@@ -597,9 +888,9 @@ 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());
|
||||
@@ -615,7 +906,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
childRequirement.setCategory(reqVO.getCategory());
|
||||
childRequirement.setSourceType(parentRequirement.getSourceType()); // 继承父需求来源类型
|
||||
childRequirement.setPriority(reqVO.getPriority());
|
||||
// 子需求初始状态为待分流
|
||||
// 子需求初始状态为待指派
|
||||
// 根据是否需要评审确定初始状态
|
||||
String initialStatus = Objects.equals(reqVO.getReviewRequired(), 1)
|
||||
? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
|
||||
@@ -630,8 +921,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
childRequirement.setAttachments(reqVO.getAttachments());
|
||||
requirementMapper.insert(childRequirement);
|
||||
|
||||
// 父需求状态从待分流变为实施中
|
||||
if (STATUS_PENDING_DISPATCH.equals(parentRequirement.getStatusCode())) {
|
||||
// 父需求等待执行指派动作时,一旦拆分子需求就进入实施中。
|
||||
if (isPendingDispatchActionStatus(parentRequirement.getStatusCode())) {
|
||||
ProductRequirementDO before = cloneRequirement(parentRequirement);
|
||||
String fromStatus = parentRequirement.getStatusCode();
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||
@@ -715,7 +1006,7 @@ 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();
|
||||
}
|
||||
@@ -787,50 +1078,17 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 该方法作用和getAllowedTransitions()类似,是用来获取当前状态下可以进行的动作
|
||||
*
|
||||
* @param requirementId 需求编号
|
||||
* @param productId 产品编号
|
||||
* @return ProductRequirementLifecycleRespVO
|
||||
* @deprecated 产品需求页面最开始用来下拉框改状态时使用的,已经弃用
|
||||
*/
|
||||
@Override
|
||||
@Deprecated
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
||||
String currentStatus = requirement.getStatusCode();
|
||||
|
||||
// 查询当前状态定义
|
||||
ObjectStatusModelDO statusModel = statusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(REQUIREMENT_OBJECT_TYPE, currentStatus);
|
||||
if (statusModel == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
|
||||
ProductRequirementLifecycleRespVO lifecycle = new ProductRequirementLifecycleRespVO();
|
||||
lifecycle.setStatusCode(statusModel.getStatusCode());
|
||||
lifecycle.setStatusName(statusModel.getStatusName());
|
||||
lifecycle.setTerminal(statusModel.getTerminalFlag());
|
||||
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)
|
||||
@@ -954,6 +1212,9 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
* 当前只对取消动作做额外过滤,避免前端展示点了也会报错的按钮。
|
||||
*/
|
||||
private boolean shouldExposeTransition(ProductRequirementDO requirement, ObjectStatusTransitionDO transition) {
|
||||
if (!isReviewRejectedActionAllowed(requirement, transition.getActionCode())) {
|
||||
return false;
|
||||
}
|
||||
if (!ACTION_CANCEL.equals(transition.getActionCode())) {
|
||||
return true;
|
||||
}
|
||||
@@ -963,6 +1224,28 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
return isParentCancelAllowed(requirement.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 评审不通过后的动作由来源决定:手工新增只能取消,工单流转只能拒绝。
|
||||
*/
|
||||
private void validateReviewRejectedActionAllowed(ProductRequirementDO requirement, String actionCode) {
|
||||
if (!isReviewRejectedActionAllowed(requirement, actionCode)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isReviewRejectedActionAllowed(ProductRequirementDO requirement, String actionCode) {
|
||||
if (!STATUS_REVIEW_REJECTED.equals(requirement.getStatusCode())) {
|
||||
return true;
|
||||
}
|
||||
if (SOURCE_TYPE_MANUAL.equals(requirement.getSourceType())) {
|
||||
return ACTION_CANCEL.equals(actionCode);
|
||||
}
|
||||
if (SOURCE_TYPE_WORK_ORDER.equals(requirement.getSourceType())) {
|
||||
return ACTION_REJECT.equals(actionCode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 父需求存在子需求时,只有全部子需求都已取消或已拒绝,才允许展示取消动作。
|
||||
*/
|
||||
@@ -1156,6 +1439,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
respVO.setSort(statusModel.getSort());
|
||||
respVO.setInitialFlag(statusModel.getInitialFlag());
|
||||
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
||||
respVO.setAllowEdit(statusModel.getAllowEdit());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@@ -1177,11 +1461,6 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
ObjectStatusModelDO statusModel = statusModelMap.get(requirement.getStatusCode());
|
||||
if (statusModel != null) {
|
||||
respVO.setStatusName(statusModel.getStatusName());
|
||||
respVO.setTerminal(statusModel.getTerminalFlag());
|
||||
}
|
||||
// 设置是否终态
|
||||
if (respVO.getTerminal() == null) {
|
||||
respVO.setTerminal(TERMINAL_STATUSES.contains(requirement.getStatusCode()));
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
@@ -1216,22 +1495,32 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验需求是否允许编辑(终态不允许编辑)
|
||||
* 校验需求是否允许编辑,编辑能力以状态模型 allow_edit 为准。
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateRequirementEditable(ProductRequirementDO requirement) {
|
||||
if (TERMINAL_STATUSES.contains(requirement.getStatusCode())) {
|
||||
ObjectStatusModelDO statusModel = statusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||
REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode());
|
||||
if (statusModel == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getTerminalStatusCodes() {
|
||||
return new HashSet<>(statusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验父需求是否允许拆分
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateParentAllowSplit(ProductRequirementDO parentRequirement) {
|
||||
String status = parentRequirement.getStatusCode();
|
||||
if (!STATUS_PENDING_DISPATCH.equals(status) && !STATUS_IMPLEMENTING.equals(status)) {
|
||||
if (!STATUS_PENDING_DISPATCH.equals(status) && !STATUS_IMPLEMENTING.equals(status)
|
||||
&& !STATUS_REVIEWED.equals(status)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT);
|
||||
}
|
||||
}
|
||||
@@ -1494,7 +1783,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
// 查询关联项目下的根模块(parentId = 0)
|
||||
ProjectRequirementModuleDO rootModule = projectRequirementModuleMapper.selectByProjectIdAndParentId(implementProjectId, 0L);
|
||||
if (rootModule == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_ROOT_NOT_EXISTS);
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_PROJECT_MODULE_ROOT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 构建项目需求记录
|
||||
@@ -1506,7 +1795,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
newRequirement.setSourceType("product_requirement");
|
||||
newRequirement.setStatusCode(STATUS_IMPLEMENTING);
|
||||
newRequirement.setReviewRequired(0); //从产品需求流转到项目需求的需求肯定不需要评审
|
||||
// 拷贝产品需求的其他字段(不拷贝排序、状态原因、更新人、更新时间、逻辑删除字段)
|
||||
// 拷贝产品需求的其他字段(不拷贝需求负责人id和姓名、排序、状态原因、更新人、更新时间、逻辑删除字段)
|
||||
newRequirement.setTitle(productRequirement.getTitle());
|
||||
newRequirement.setDescription(productRequirement.getDescription());
|
||||
newRequirement.setCategory(productRequirement.getCategory());
|
||||
@@ -1515,8 +1804,6 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
newRequirement.setProposerId(productRequirement.getProposerId());
|
||||
newRequirement.setProposerNickname(productRequirement.getProposerNickname());
|
||||
newRequirement.setExpectedTime(productRequirement.getExpectedTime());
|
||||
newRequirement.setCurrentHandlerUserId(productRequirement.getCurrentHandlerUserId());
|
||||
newRequirement.setCurrentHandlerUserNickname(productRequirement.getCurrentHandlerUserNickname());
|
||||
newRequirement.setAttachments(productRequirement.getAttachments());
|
||||
newRequirement.setCreator(productRequirement.getCreator());
|
||||
newRequirement.setCreateTime(productRequirement.getCreateTime());
|
||||
@@ -1525,3 +1812,4 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementAllowedTransitionBatchRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
||||
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;
|
||||
@@ -23,94 +22,40 @@ import java.util.List;
|
||||
*/
|
||||
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 changeRequirementStatusForReview(ProjectRequirementStatusActionReqVO reqVO);
|
||||
|
||||
void deleteRequirement(Long id, Long projectId);
|
||||
|
||||
/**
|
||||
* 拆分需求
|
||||
*/
|
||||
Long splitRequirement(ProjectRequirementSplitReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 关闭需求
|
||||
*/
|
||||
void closeRequirement(ProjectRequirementCloseReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 获取需求可执行动作列表
|
||||
*/
|
||||
List<ProjectRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long projectId);
|
||||
|
||||
/**
|
||||
* 批量获取需求可执行动作列表
|
||||
*/
|
||||
List<ProjectRequirementAllowedTransitionBatchRespVO> getAllowedTransitionsBatch(ProjectRequirementBatchReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 获取需求生命周期信息
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementAllowedTransitionBatchRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
||||
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;
|
||||
@@ -66,21 +65,23 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
private static final String REQUIREMENT_OBJECT_TYPE = "project_requirement";
|
||||
private static final String PROJECT_OBJECT_TYPE = ProjectObjectConstants.OBJECT_TYPE;
|
||||
|
||||
private static final String STATUS_PENDING_CONFIRM = "pending_confirm";
|
||||
private static final String STATUS_PENDING_CLAIM = "pending_claim";
|
||||
private static final String STATUS_PENDING_REVIEW = "pending_review";
|
||||
private static final String STATUS_REVIEWED = "reviewed";
|
||||
private static final String STATUS_REVIEW_REJECTED = "review_rejected";
|
||||
private static final String STATUS_IMPLEMENTING = "implementing";
|
||||
private static final String STATUS_ACCEPTED = "accepted";
|
||||
private static final String STATUS_CLOSED = "closed";
|
||||
private static final String STATUS_REJECTED = "rejected";
|
||||
private static final String STATUS_CANCELLED = "cancelled";
|
||||
private static final String SOURCE_TYPE_PRODUCT_REQUIREMENT = "product_requirement";
|
||||
private static final String SOURCE_TYPE_MANUAL = "manual";
|
||||
private static final String SOURCE_TYPE_WORK_ORDER = "work_order";
|
||||
|
||||
private static final List<String> TERMINAL_STATUSES = List.of(
|
||||
STATUS_CLOSED, STATUS_REJECTED, STATUS_CANCELLED);
|
||||
private static final List<String> CHILD_ALLOW_CLOSE_STATUSES = List.of(
|
||||
STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED);
|
||||
private static final Set<String> REVIEW_STATUS_CODES = Set.of(
|
||||
STATUS_PENDING_REVIEW, STATUS_REVIEWED, STATUS_REVIEW_REJECTED);
|
||||
private static final List<String> ALLOW_DELETE_STATUSES = List.of(
|
||||
STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW);
|
||||
STATUS_PENDING_CLAIM, STATUS_PENDING_REVIEW);
|
||||
private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(
|
||||
STATUS_REJECTED, STATUS_CANCELLED);
|
||||
|
||||
@@ -88,6 +89,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
private static final String PROJECT_QUERY_PERMISSION = "project:project:query";
|
||||
private static final String PROJECT_UPDATE_PERMISSION = ProjectObjectConstants.PERMISSION_UPDATE;
|
||||
private static final String PROJECT_STATUS_PERMISSION = ProjectObjectConstants.PERMISSION_STATUS;
|
||||
private static final String PROJECT_REVIEW_PERMISSION = ProjectObjectConstants.PERMISSION_REVIEW;
|
||||
private static final String PROJECT_DELETE_PERMISSION = ProjectObjectConstants.PERMISSION_DELETE;
|
||||
private static final String PROJECT_SPLIT_PERMISSION = ProjectObjectConstants.PERMISSION_SPLIT;
|
||||
|
||||
@@ -98,6 +100,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
private static final String ACTION_CLOSE = "close";
|
||||
private static final String ACTION_ACCEPT = "accept";
|
||||
private static final String ACTION_CANCEL = "cancel";
|
||||
private static final String ACTION_REJECT = "reject";
|
||||
private static final String ACTION_PASS_REVIEW = "pass_review";
|
||||
private static final String ACTION_REJECT_REVIEW = "reject_review";
|
||||
|
||||
private static final String ACTION_AUTO_DERIVE = "auto_derive";
|
||||
private static final String ACTION_SYNC_PRODUCT_STATUS = "sync_project_requirement_status";
|
||||
@@ -145,7 +150,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
requirement.setTitle(createReqVO.getTitle().trim());
|
||||
requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
|
||||
requirement.setCategory(createReqVO.getCategory());
|
||||
requirement.setSourceType("manual");
|
||||
requirement.setSourceType(SOURCE_TYPE_MANUAL);
|
||||
requirement.setPriority(createReqVO.getPriority());
|
||||
String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
|
||||
? STATUS_PENDING_REVIEW : STATUS_IMPLEMENTING;
|
||||
@@ -171,7 +176,11 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
public void updateRequirement(ProjectRequirementUpdateReqVO updateReqVO) {
|
||||
ProjectRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
|
||||
validateRequirementBelongsToProject(requirement, updateReqVO.getProjectId());
|
||||
validateRequirementEditable(requirement);
|
||||
if (!(SOURCE_TYPE_PRODUCT_REQUIREMENT.equals(requirement.getSourceType())
|
||||
&& Objects.equals(requirement.getParentId(), 0L)
|
||||
&& STATUS_IMPLEMENTING.equals(requirement.getStatusCode()))) {
|
||||
validateRequirementEditable(requirement);
|
||||
}
|
||||
|
||||
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProjectId());
|
||||
validateModuleBelongsToProject(moduleId, updateReqVO.getProjectId());
|
||||
@@ -292,6 +301,19 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
|
||||
permission = PROJECT_STATUS_PERMISSION)
|
||||
public void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO) {
|
||||
doChangeRequirementStatus(reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
|
||||
permission = PROJECT_REVIEW_PERMISSION)
|
||||
public void changeRequirementStatusForReview(ProjectRequirementStatusActionReqVO reqVO) {
|
||||
validateReviewStatusAction(reqVO.getActionCode());
|
||||
doChangeRequirementStatus(reqVO);
|
||||
}
|
||||
|
||||
private void doChangeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO) {
|
||||
ProjectRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
||||
validateRequirementBelongsToProject(requirement, reqVO.getProjectId());
|
||||
String actionCode = reqVO.getActionCode().trim();
|
||||
@@ -300,6 +322,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
ProjectRequirementDO before = cloneRequirement(requirement);
|
||||
|
||||
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
||||
validateReviewRejectedActionAllowed(requirement, actionCode);
|
||||
validateTransitionReason(transition, reason);
|
||||
String toStatus = transition.getToStatusCode();
|
||||
|
||||
@@ -330,6 +353,13 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
refreshAncestorStatusAndSyncProduct(requirement.getId());
|
||||
}
|
||||
|
||||
private void validateReviewStatusAction(String actionCode) {
|
||||
String normalizedActionCode = actionCode == null ? null : actionCode.trim();
|
||||
if (!ACTION_PASS_REVIEW.equals(normalizedActionCode) && !ACTION_REJECT_REVIEW.equals(normalizedActionCode)) {
|
||||
throw invalidParamException("评审权限只能触发评审通过或评审不通过动作");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId",
|
||||
@@ -371,7 +401,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
if (!Objects.equals(requirement.getProjectId(), projectId)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_NOT_BELONG_TO_PROJECT);
|
||||
}
|
||||
if (TERMINAL_STATUSES.contains(requirement.getStatusCode())) {
|
||||
if (getTerminalStatusCodes().contains(requirement.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_TERMINAL);
|
||||
}
|
||||
}
|
||||
@@ -416,8 +446,23 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
|
||||
writeBizAuditLog(childRequirement, ACTION_CREATE, null, initialStatus,
|
||||
buildRequirementFieldChanges(null, childRequirement), null);
|
||||
writeBizAuditLog(parentRequirement, ACTION_SPLIT, parentRequirement.getStatusCode(),
|
||||
parentRequirement.getStatusCode(), null, null);
|
||||
if (STATUS_REVIEWED.equals(parentRequirement.getStatusCode())) {
|
||||
ProjectRequirementDO before = cloneRequirement(parentRequirement);
|
||||
String fromStatus = parentRequirement.getStatusCode();
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||
parentRequirement.getId(), fromStatus, STATUS_IMPLEMENTING, null);
|
||||
if (updateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
parentRequirement.setStatusCode(STATUS_IMPLEMENTING);
|
||||
parentRequirement.setLastStatusReason(null);
|
||||
writeRequirementStatusLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null);
|
||||
writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING,
|
||||
buildRequirementFieldChanges(before, parentRequirement), null);
|
||||
} else {
|
||||
writeBizAuditLog(parentRequirement, ACTION_SPLIT, parentRequirement.getStatusCode(),
|
||||
parentRequirement.getStatusCode(), null, null);
|
||||
}
|
||||
return childRequirement.getId();
|
||||
}
|
||||
|
||||
@@ -467,16 +512,16 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
// 取消动作不满足前置条件时,不再返回给前端展示按钮
|
||||
.filter(transition -> shouldExposeTransition(requirement, transition))
|
||||
.map(transition -> {
|
||||
ProjectRequirementStatusTransitionRespVO vo = new ProjectRequirementStatusTransitionRespVO();
|
||||
vo.setActionCode(transition.getActionCode());
|
||||
vo.setActionName(transition.getActionName());
|
||||
vo.setToStatusCode(transition.getToStatusCode());
|
||||
ObjectStatusModelDO statusModel = statusModelMapper
|
||||
.selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, transition.getToStatusCode());
|
||||
vo.setToStatusName(statusModel != null ? statusModel.getStatusName() : transition.getToStatusCode());
|
||||
vo.setNeedReason(transition.getNeedReason());
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
ProjectRequirementStatusTransitionRespVO vo = new ProjectRequirementStatusTransitionRespVO();
|
||||
vo.setActionCode(transition.getActionCode());
|
||||
vo.setActionName(transition.getActionName());
|
||||
vo.setToStatusCode(transition.getToStatusCode());
|
||||
ObjectStatusModelDO statusModel = statusModelMapper
|
||||
.selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, transition.getToStatusCode());
|
||||
vo.setToStatusName(statusModel != null ? statusModel.getStatusName() : transition.getToStatusCode());
|
||||
vo.setNeedReason(transition.getNeedReason());
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -496,29 +541,6 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = PROJECT_QUERY_PERMISSION)
|
||||
public ProjectRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long projectId) {
|
||||
ProjectRequirementDO requirement = validateRequirementExists(requirementId);
|
||||
validateRequirementBelongsToProject(requirement, projectId);
|
||||
String currentStatus = requirement.getStatusCode();
|
||||
|
||||
ObjectStatusModelDO statusModel = statusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(REQUIREMENT_OBJECT_TYPE, currentStatus);
|
||||
if (statusModel == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
|
||||
ProjectRequirementLifecycleRespVO lifecycle = new ProjectRequirementLifecycleRespVO();
|
||||
lifecycle.setStatusCode(statusModel.getStatusCode());
|
||||
lifecycle.setStatusName(statusModel.getStatusName());
|
||||
lifecycle.setTerminal(statusModel.getTerminalFlag());
|
||||
lifecycle.setAllowEdit(statusModel.getAllowEdit());
|
||||
lifecycle.setLastStatusReason(requirement.getLastStatusReason());
|
||||
lifecycle.setAvailableActions(getAllowedTransitions(requirementId, projectId));
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -812,9 +834,11 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
@VisibleForTesting
|
||||
void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
|
||||
List<ProjectRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
||||
Set<String> terminalStatusCodes = getTerminalStatusCodes();
|
||||
for (ProjectRequirementDO requirement : allChildren) {
|
||||
if (!Objects.equals(requirement.getId(), requirementId)
|
||||
&& !CHILD_ALLOW_CLOSE_STATUSES.contains(requirement.getStatusCode())) {
|
||||
&& !STATUS_ACCEPTED.equals(requirement.getStatusCode())
|
||||
&& !terminalStatusCodes.contains(requirement.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
|
||||
}
|
||||
}
|
||||
@@ -838,6 +862,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
* 当前只对取消动作做额外过滤,避免前端展示点了也会报错的按钮。
|
||||
*/
|
||||
private boolean shouldExposeTransition(ProjectRequirementDO requirement, ObjectStatusTransitionDO transition) {
|
||||
if (!isReviewRejectedActionAllowed(requirement, transition.getActionCode())) {
|
||||
return false;
|
||||
}
|
||||
if (!ACTION_CANCEL.equals(transition.getActionCode())) {
|
||||
return true;
|
||||
}
|
||||
@@ -850,12 +877,36 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
return isParentCancelAllowed(requirement.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 评审不通过后的动作由来源决定:手工新增和产品需求子需求只能取消,工单流转只能拒绝。
|
||||
*/
|
||||
private void validateReviewRejectedActionAllowed(ProjectRequirementDO requirement, String actionCode) {
|
||||
if (!isReviewRejectedActionAllowed(requirement, actionCode)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isReviewRejectedActionAllowed(ProjectRequirementDO requirement, String actionCode) {
|
||||
if (!STATUS_REVIEW_REJECTED.equals(requirement.getStatusCode())) {
|
||||
return true;
|
||||
}
|
||||
if (SOURCE_TYPE_MANUAL.equals(requirement.getSourceType())
|
||||
|| SOURCE_TYPE_PRODUCT_REQUIREMENT.equals(requirement.getSourceType())) {
|
||||
return ACTION_CANCEL.equals(actionCode);
|
||||
}
|
||||
if (SOURCE_TYPE_WORK_ORDER.equals(requirement.getSourceType())) {
|
||||
return ACTION_REJECT.equals(actionCode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前项目需求是否由产品需求流转生成。
|
||||
*/
|
||||
private boolean isFromProductRequirement(ProjectRequirementDO requirement) {
|
||||
return Objects.equals(requirement.getSourceType(), SOURCE_TYPE_PRODUCT_REQUIREMENT)
|
||||
&& requirement.getProductRequirementId() != null;
|
||||
&& requirement.getProductRequirementId() != null
|
||||
&& Objects.equals(requirement.getParentId(), 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -996,6 +1047,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
|| rootRequirement.getProductRequirementId() == null) {
|
||||
return;
|
||||
}
|
||||
if (REVIEW_STATUS_CODES.contains(rootRequirement.getStatusCode())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProductRequirementDO productRequirement = productRequirementMapper.selectById(rootRequirement.getProductRequirementId());
|
||||
if (productRequirement == null || Objects.equals(productRequirement.getStatusCode(), rootRequirement.getStatusCode())) {
|
||||
@@ -1039,6 +1093,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
respVO.setSort(statusModel.getSort());
|
||||
respVO.setInitialFlag(statusModel.getInitialFlag());
|
||||
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
||||
respVO.setAllowEdit(statusModel.getAllowEdit());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@@ -1058,10 +1113,6 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
ObjectStatusModelDO statusModel = statusModelMap.get(requirement.getStatusCode());
|
||||
if (statusModel != null) {
|
||||
respVO.setStatusName(statusModel.getStatusName());
|
||||
respVO.setTerminal(statusModel.getTerminalFlag());
|
||||
}
|
||||
if (respVO.getTerminal() == null) {
|
||||
respVO.setTerminal(TERMINAL_STATUSES.contains(requirement.getStatusCode()));
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
@@ -1219,14 +1270,24 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
|
||||
@VisibleForTesting
|
||||
void validateRequirementEditable(ProjectRequirementDO requirement) {
|
||||
if (TERMINAL_STATUSES.contains(requirement.getStatusCode())) {
|
||||
ObjectStatusModelDO statusModel = statusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||
REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode());
|
||||
if (statusModel == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getTerminalStatusCodes() {
|
||||
return new HashSet<>(statusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateParentAllowSplit(ProjectRequirementDO parentRequirement) {
|
||||
if (!STATUS_IMPLEMENTING.equals(parentRequirement.getStatusCode())) {
|
||||
String status = parentRequirement.getStatusCode();
|
||||
if (!STATUS_IMPLEMENTING.equals(status)&& !STATUS_REVIEWED.equals(status)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_PARENT_NOT_ALLOW_SPLIT);
|
||||
}
|
||||
}
|
||||
@@ -1541,4 +1602,4 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
return StringUtils.hasText(value) ? value : "";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.njcn.rdms.module.project.service.review;
|
||||
|
||||
import com.njcn.rdms.module.project.controller.admin.review.vo.RequirementReviewRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.review.vo.RequirementReviewSubmitReqVO;
|
||||
|
||||
public interface RequirementReviewService {
|
||||
|
||||
Long submitProductRequirementReview(RequirementReviewSubmitReqVO reqVO);
|
||||
|
||||
RequirementReviewRespVO getProductRequirementReview(Long productId, Long requirementId);
|
||||
|
||||
Long submitProjectRequirementReview(RequirementReviewSubmitReqVO reqVO);
|
||||
|
||||
RequirementReviewRespVO getProjectRequirementReview(Long projectId, Long requirementId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package com.njcn.rdms.module.project.service.review;
|
||||
|
||||
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.review.vo.RequirementReviewRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.review.vo.RequirementReviewSubmitReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.review.RequirementReviewDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.review.RequirementReviewMapper;
|
||||
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 com.njcn.rdms.module.project.service.product.ProductRequirementService;
|
||||
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
|
||||
/**
|
||||
* 需求评审 Service 实现类。
|
||||
*/
|
||||
@Service
|
||||
public class RequirementReviewServiceImpl implements RequirementReviewService {
|
||||
|
||||
private static final String PRODUCT_REQUIREMENT_OBJECT_TYPE = "product_requirement";
|
||||
private static final String PROJECT_REQUIREMENT_OBJECT_TYPE = "project_requirement";
|
||||
private static final int CONCLUSION_PASS = 0;
|
||||
private static final int CONCLUSION_REJECT = 1;
|
||||
private static final String ACTION_PASS_REVIEW = "pass_review";
|
||||
private static final String ACTION_REJECT_REVIEW = "reject_review";
|
||||
|
||||
@Resource
|
||||
private RequirementReviewMapper reviewMapper;
|
||||
@Resource
|
||||
private ProductRequirementMapper productRequirementMapper;
|
||||
@Resource
|
||||
private ProjectRequirementMapper projectRequirementMapper;
|
||||
@Resource
|
||||
private ProductRequirementService productRequirementService;
|
||||
@Resource
|
||||
private ProjectRequirementService projectRequirementService;
|
||||
@Resource
|
||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = ProductObjectConstants.PERMISSION_REVIEW)
|
||||
public Long submitProductRequirementReview(RequirementReviewSubmitReqVO reqVO) {
|
||||
validateProductRequirement(reqVO);
|
||||
validateConclusion(reqVO.getConclusion(), true);
|
||||
validateReviewNotExists(PRODUCT_REQUIREMENT_OBJECT_TYPE, reqVO.getRequirementId(), true);
|
||||
AttachmentValidator.validate(reqVO.getAttachments());
|
||||
attachmentFileIdResolver.resolve(reqVO.getAttachments());
|
||||
|
||||
RequirementReviewDO review = buildReview(reqVO, PRODUCT_REQUIREMENT_OBJECT_TYPE);
|
||||
reviewMapper.insert(review);
|
||||
|
||||
ProductRequirementStatusActionReqVO actionReqVO = new ProductRequirementStatusActionReqVO();
|
||||
actionReqVO.setId(reqVO.getRequirementId());
|
||||
actionReqVO.setProductId(reqVO.getProductId());
|
||||
actionReqVO.setActionCode(resolveReviewAction(reqVO.getConclusion()));
|
||||
productRequirementService.changeRequirementStatusForReview(actionReqVO);
|
||||
return review.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
|
||||
permission = "project:product:query")
|
||||
public RequirementReviewRespVO getProductRequirementReview(Long productId, Long requirementId) {
|
||||
ProductRequirementDO requirement = productRequirementMapper.selectById(requirementId);
|
||||
if (requirement == null || !Objects.equals(requirement.getProductId(), productId)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS);
|
||||
}
|
||||
RequirementReviewDO review = reviewMapper.selectByObjectTypeAndRequirementId(
|
||||
PRODUCT_REQUIREMENT_OBJECT_TYPE, requirementId);
|
||||
if (review == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_REVIEW_NOT_EXISTS);
|
||||
}
|
||||
return buildRespVO(review);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#reqVO.projectId",
|
||||
permission = ProjectObjectConstants.PERMISSION_REVIEW)
|
||||
public Long submitProjectRequirementReview(RequirementReviewSubmitReqVO reqVO) {
|
||||
validateProjectRequirement(reqVO);
|
||||
validateConclusion(reqVO.getConclusion(), false);
|
||||
validateReviewNotExists(PROJECT_REQUIREMENT_OBJECT_TYPE, reqVO.getRequirementId(), false);
|
||||
AttachmentValidator.validate(reqVO.getAttachments());
|
||||
attachmentFileIdResolver.resolve(reqVO.getAttachments());
|
||||
|
||||
RequirementReviewDO review = buildReview(reqVO, PROJECT_REQUIREMENT_OBJECT_TYPE);
|
||||
reviewMapper.insert(review);
|
||||
|
||||
ProjectRequirementStatusActionReqVO actionReqVO = new ProjectRequirementStatusActionReqVO();
|
||||
actionReqVO.setId(reqVO.getRequirementId());
|
||||
actionReqVO.setProjectId(reqVO.getProjectId());
|
||||
actionReqVO.setActionCode(resolveReviewAction(reqVO.getConclusion()));
|
||||
projectRequirementService.changeRequirementStatusForReview(actionReqVO);
|
||||
return review.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = "project:project:query")
|
||||
public RequirementReviewRespVO getProjectRequirementReview(Long projectId, Long requirementId) {
|
||||
ProjectRequirementDO requirement = projectRequirementMapper.selectById(requirementId);
|
||||
if (requirement == null || !Objects.equals(requirement.getProjectId(), projectId)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_NOT_EXISTS);
|
||||
}
|
||||
RequirementReviewDO review = reviewMapper.selectByObjectTypeAndRequirementId(
|
||||
PROJECT_REQUIREMENT_OBJECT_TYPE, requirementId);
|
||||
if (review == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_REVIEW_NOT_EXISTS);
|
||||
}
|
||||
return buildRespVO(review);
|
||||
}
|
||||
|
||||
private void validateProductRequirement(RequirementReviewSubmitReqVO reqVO) {
|
||||
if (reqVO.getProductId() == null) {
|
||||
throw invalidParamException("产品编号不能为空");
|
||||
}
|
||||
ProductRequirementDO requirement = productRequirementMapper.selectById(reqVO.getRequirementId());
|
||||
if (requirement == null || !Objects.equals(requirement.getProductId(), reqVO.getProductId())) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateProjectRequirement(RequirementReviewSubmitReqVO reqVO) {
|
||||
if (reqVO.getProjectId() == null) {
|
||||
throw invalidParamException("项目编号不能为空");
|
||||
}
|
||||
ProjectRequirementDO requirement = projectRequirementMapper.selectById(reqVO.getRequirementId());
|
||||
if (requirement == null || !Objects.equals(requirement.getProjectId(), reqVO.getProjectId())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateReviewNotExists(String objectType, Long requirementId, boolean productRequirement) {
|
||||
RequirementReviewDO review = reviewMapper.selectByObjectTypeAndRequirementId(objectType, requirementId);
|
||||
if (review != null) {
|
||||
throw exception(productRequirement
|
||||
? ErrorCodeConstants.REQUIREMENT_REVIEW_ALREADY_EXISTS
|
||||
: ErrorCodeConstants.PROJECT_REQUIREMENT_REVIEW_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateConclusion(Integer conclusion, boolean productRequirement) {
|
||||
if (!Objects.equals(conclusion, CONCLUSION_PASS) && !Objects.equals(conclusion, CONCLUSION_REJECT)) {
|
||||
throw exception(productRequirement
|
||||
? ErrorCodeConstants.REQUIREMENT_REVIEW_CONCLUSION_INVALID
|
||||
: ErrorCodeConstants.PROJECT_REQUIREMENT_REVIEW_CONCLUSION_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
private RequirementReviewDO buildReview(RequirementReviewSubmitReqVO reqVO, String objectType) {
|
||||
RequirementReviewDO review = new RequirementReviewDO();
|
||||
review.setObjectType(objectType);
|
||||
review.setRequirementId(reqVO.getRequirementId());
|
||||
review.setOperatorId(reqVO.getOperatorId());
|
||||
review.setConclusion(reqVO.getConclusion());
|
||||
review.setReviewContent(reqVO.getReviewContent());
|
||||
review.setRequirementEstimatedHours(reqVO.getRequirementEstimatedHours());
|
||||
review.setAttendees(reqVO.getAttendees());
|
||||
review.setAttachments(reqVO.getAttachments());
|
||||
review.setReviewTime(toReviewTime(reqVO.getReviewTime()));
|
||||
return review;
|
||||
}
|
||||
|
||||
private RequirementReviewRespVO buildRespVO(RequirementReviewDO review) {
|
||||
RequirementReviewRespVO respVO = new RequirementReviewRespVO();
|
||||
respVO.setId(review.getId());
|
||||
respVO.setObjectType(review.getObjectType());
|
||||
respVO.setRequirementId(review.getRequirementId());
|
||||
respVO.setOperatorId(review.getOperatorId());
|
||||
respVO.setConclusion(review.getConclusion());
|
||||
respVO.setReviewContent(review.getReviewContent());
|
||||
respVO.setRequirementEstimatedHours(review.getRequirementEstimatedHours());
|
||||
respVO.setAttendees(review.getAttendees());
|
||||
respVO.setAttachments(review.getAttachments());
|
||||
respVO.setReviewTime(review.getReviewTime() == null ? null : review.getReviewTime().toLocalDate());
|
||||
respVO.setCreateTime(review.getCreateTime());
|
||||
respVO.setUpdateTime(review.getUpdateTime());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private String resolveReviewAction(Integer conclusion) {
|
||||
return Objects.equals(conclusion, CONCLUSION_PASS) ? ACTION_PASS_REVIEW : ACTION_REJECT_REVIEW;
|
||||
}
|
||||
|
||||
private LocalDateTime toReviewTime(LocalDate reviewTime) {
|
||||
LocalDate date = reviewTime == null ? LocalDate.now() : reviewTime;
|
||||
return date.atStartOfDay();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).insert(captor.capture());
|
||||
ProductRequirementDO created = captor.getValue();
|
||||
assertEquals("pending_dispatch", created.getStatusCode()); // 不需要评审时初始状态为待分流
|
||||
assertEquals("pending_dispatch", created.getStatusCode()); // 不需要评审时初始状态为待指派
|
||||
assertEquals("manual", created.getSourceType());
|
||||
assertEquals(0L, created.getParentId());
|
||||
assertEquals("测试需求", created.getTitle());
|
||||
@@ -151,7 +151,7 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
Long requirementId = 1002L;
|
||||
Long loginUserId = 1001L;
|
||||
Long defaultModuleId = 50L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
@@ -183,7 +183,7 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
void changeRequirementStatus_whenActionAllowed_shouldUpdateStatusAndWriteLogs() {
|
||||
Long requirementId = 1003L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
@@ -229,7 +229,7 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
@Test
|
||||
void changeRequirementStatus_whenReasonRequiredButMissing_shouldThrowException() {
|
||||
Long requirementId = 1005L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
@@ -250,7 +250,7 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
void changeRequirementStatus_whenConcurrentModified_shouldThrowException() {
|
||||
Long requirementId = 1006L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
@@ -278,7 +278,7 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
void deleteRequirement_shouldDeleteByConditionAndWriteLogs() {
|
||||
Long requirementId = 1007L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
|
||||
Reference in New Issue
Block a user