From 58eed8234ad334ec88e556f5ea4c9d75eb37c9ea Mon Sep 17 00:00:00 2001 From: dk <1260500659@qq.com> Date: Fri, 22 May 2026 13:45:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=B0=E5=A2=9E=E9=9C=80=E6=B1=82?= =?UTF-8?q?=E8=AF=84=E5=AE=A1=E5=8A=9F=E8=83=BD):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=9C=80=E6=B1=82=E8=AF=84=E5=AE=A1=E5=8A=9F=E8=83=BD=E3=80=82?= =?UTF-8?q?=20fix(=E4=BA=A7=E5=93=81=E9=9C=80=E6=B1=82=E3=80=81=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=9C=80=E6=B1=82):=20=E6=8C=89=E7=85=A7=E4=BC=9A?= =?UTF-8?q?=E8=AE=AE=E6=84=8F=E8=A7=81=E4=BF=AE=E6=94=B9=E8=AF=B8=E5=A4=9A?= =?UTF-8?q?=E7=BB=86=E8=8A=82=E3=80=82=20fix(=E4=BA=A7=E5=93=81=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E5=9F=9F=E7=9A=84=E6=A6=82=E8=A7=88=E7=95=8C=E9=9D=A2?= =?UTF-8?q?):=20=E6=8F=90=E4=BE=9B=E7=9B=B8=E5=BA=94=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E7=BB=99=E5=89=8D=E7=AB=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biz/system/dict/dto/DictDataRespDTO.java | 3 - .../project/enums/ErrorCodeConstants.java | 28 +- .../constant/ProductObjectConstants.java | 5 + .../constant/ProjectObjectConstants.java | 5 + .../product/ProductRequirementController.java | 22 +- ...equirementDashboardRecentChangeRespVO.java | 42 ++ .../ProductRequirementDashboardRespVO.java | 21 + ...ductRequirementDashboardSummaryRespVO.java | 37 ++ ...equirementDispatchedProjectLinkRespVO.java | 4 +- ...ctRequirementHasDispatchedBatchRespVO.java | 4 +- .../ProductRequirementLifecycleRespVO.java | 33 -- .../requirement/ProductRequirementRespVO.java | 9 +- .../ProductRequirementStatusActionReqVO.java | 2 +- .../ProductRequirementStatusDictRespVO.java | 7 +- ...ductRequirementStatusTransitionRespVO.java | 2 +- .../project/ProjectRequirementController.java | 15 +- .../ProjectRequirementLifecycleRespVO.java | 33 -- .../requirement/ProjectRequirementRespVO.java | 2 +- .../ProjectRequirementStatusActionReqVO.java | 2 +- .../ProjectRequirementStatusDictRespVO.java | 7 +- ...jectRequirementStatusTransitionRespVO.java | 2 +- .../review/RequirementReviewController.java | 62 +++ .../review/vo/RequirementReviewRespVO.java | 53 +++ .../vo/RequirementReviewSubmitReqVO.java | 54 +++ .../product/ProductRequirementDO.java | 2 +- .../ProductRequirementStatusLogDO.java | 6 +- .../review/RequirementReviewAttendeeItem.java | 19 + .../review/RequirementReviewDO.java | 50 +++ .../ProductRequirementStatusLogMapper.java | 17 + .../mysql/review/RequirementReviewMapper.java | 17 + .../product/ProductRequirementService.java | 131 +----- .../ProductRequirementServiceImpl.java | 422 +++++++++++++++--- .../project/ProjectRequirementService.java | 59 +-- .../ProjectRequirementServiceImpl.java | 169 ++++--- .../review/RequirementReviewService.java | 16 + .../review/RequirementReviewServiceImpl.java | 211 +++++++++ .../ProductRequirementServiceImplTest.java | 12 +- .../admin/dict/vo/data/DictDataRespVO.java | 4 - .../admin/dict/vo/data/DictDataSaveReqVO.java | 4 - .../dict/vo/data/DictDataSimpleRespVO.java | 3 - .../app/dict/vo/AppDictDataRespVO.java | 3 - .../dal/dataobject/dict/DictDataDO.java | 5 - 42 files changed, 1142 insertions(+), 462 deletions(-) create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardRecentChangeRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardSummaryRespVO.java delete mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementLifecycleRespVO.java delete mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementLifecycleRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/RequirementReviewController.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/vo/RequirementReviewRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/vo/RequirementReviewSubmitReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/review/RequirementReviewAttendeeItem.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/review/RequirementReviewDO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/review/RequirementReviewMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/review/RequirementReviewService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/review/RequirementReviewServiceImpl.java diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/dto/DictDataRespDTO.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/dto/DictDataRespDTO.java index c5068d4..179dcfb 100644 --- a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/dto/DictDataRespDTO.java +++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/biz/system/dict/dto/DictDataRespDTO.java @@ -19,7 +19,4 @@ public class DictDataRespDTO { @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; // 参见 CommonStatusEnum 枚举 - @Schema(description = "标识", example = "system") - private String sign; - } diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java index 541f577..8898796 100644 --- a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java +++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java @@ -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, "个人事项不存在"); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProductObjectConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProductObjectConstants.java index e13de37..54a03d6 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProductObjectConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProductObjectConstants.java @@ -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"; + /** * 产品删除权限码。 */ diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java index 2106807..8797db9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java @@ -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"; + /** * 项目拆分权限码。 */ diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java index 6877501..4d11f6b 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java @@ -28,8 +28,6 @@ public class ProductRequirementController { @Resource private ProductRequirementService requirementService; - // ========== 需求管理 ========== - @PostMapping("/create") @Operation(summary = "创建产品需求") public CommonResult createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) { @@ -59,11 +57,18 @@ public class ProductRequirementController { } @GetMapping("/tree") - @Operation(summary = "获取需求树形列表(分页)") + @Operation(summary = "获取需求树分页列表") public CommonResult> getRequirementTree(@Valid ProductRequirementPageReqVO pageReqVO) { return success(requirementService.getRequirementTree(pageReqVO)); } + @GetMapping("/dashboard") + @Operation(summary = "获取产品需求概览数据") + @Parameter(name = "productId", description = "产品编号", required = true, example = "1024") + public CommonResult getRequirementDashboard(@RequestParam("productId") Long productId) { + return success(requirementService.getRequirementDashboard(productId)); + } + @PostMapping("/change-status") @Operation(summary = "变更需求状态") public CommonResult 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 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 createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardRecentChangeRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardRecentChangeRespVO.java new file mode 100644 index 0000000..3d705f1 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardRecentChangeRespVO.java @@ -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; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardRespVO.java new file mode 100644 index 0000000..1d6a128 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardRespVO.java @@ -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 recentChanges; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardSummaryRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardSummaryRespVO.java new file mode 100644 index 0000000..1a9f4ef --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDashboardSummaryRespVO.java @@ -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; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDispatchedProjectLinkRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDispatchedProjectLinkRespVO.java index 2bb17eb..2236ca4 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDispatchedProjectLinkRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementDispatchedProjectLinkRespVO.java @@ -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 { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementHasDispatchedBatchRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementHasDispatchedBatchRespVO.java index 3ebc670..eb9006c 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementHasDispatchedBatchRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementHasDispatchedBatchRespVO.java @@ -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; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementLifecycleRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementLifecycleRespVO.java deleted file mode 100644 index e8e2bab..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementLifecycleRespVO.java +++ /dev/null @@ -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 availableActions; - -} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java index 4af269e..f8e618c 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java @@ -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 children; - - @Schema(description = "是否为终态", example = "false") - private Boolean terminal; - } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusActionReqVO.java index 5c4b0fe..c553ab6 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusActionReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusActionReqVO.java @@ -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") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusDictRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusDictRespVO.java index 92cd3fd..57ca6cd 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusDictRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusDictRespVO.java @@ -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; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusTransitionRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusTransitionRespVO.java index 5dbf02a..e3ab9c8 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusTransitionRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusTransitionRespVO.java @@ -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") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java index 075fe58..57116aa 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java @@ -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> 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 getRequirementLifecycle( - @RequestParam("requirementId") Long requirementId, - @RequestParam("projectId") Long projectId) { - return success(requirementService.getRequirementLifecycle(requirementId, projectId)); - } - @PostMapping("/module/create") @Operation(summary = "创建需求模块") public CommonResult createRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) { @@ -165,7 +154,7 @@ public class ProjectRequirementController { } @GetMapping("/status/dict/terminal") - @Operation(summary = "获取需求终态状态字典") + @Operation(summary = "获取需求终止态状态字典") public CommonResult> getRequirementTerminalStatusDict() { return success(requirementService.getRequirementTerminalStatusDict()); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementLifecycleRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementLifecycleRespVO.java deleted file mode 100644 index 3192ac0..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementLifecycleRespVO.java +++ /dev/null @@ -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 availableActions; - -} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementRespVO.java index b6318ae..72b762b 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementRespVO.java @@ -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") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusActionReqVO.java index d2eb1c8..2364b8d 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusActionReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusActionReqVO.java @@ -26,7 +26,7 @@ public class ProjectRequirementStatusActionReqVO { @Size(max = 32, message = "动作编码长度不能超过 32 个字符") private String actionCode; - @Schema(description = "状态变更原因", example = "评审通过") + @Schema(description = "状态变更原因", example = "需求全部结束") private String reason; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusDictRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusDictRespVO.java index 0cfb622..373ab4f 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusDictRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusDictRespVO.java @@ -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; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusTransitionRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusTransitionRespVO.java index 1af41d4..8b8414b 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusTransitionRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/requirement/ProjectRequirementStatusTransitionRespVO.java @@ -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") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/RequirementReviewController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/RequirementReviewController.java new file mode 100644 index 0000000..588016a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/RequirementReviewController.java @@ -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 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 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 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 getProjectRequirementReview(@RequestParam("projectId") Long projectId, + @RequestParam("requirementId") Long requirementId) { + return success(reviewService.getProjectRequirementReview(projectId, requirementId)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/vo/RequirementReviewRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/vo/RequirementReviewRespVO.java new file mode 100644 index 0000000..e6eaddd --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/vo/RequirementReviewRespVO.java @@ -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 attendees; + + @Schema(description = "会议资料附件") + private List attachments; + + @Schema(description = "实际评审日期", example = "2026-05-20") + private LocalDate reviewTime; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/vo/RequirementReviewSubmitReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/vo/RequirementReviewSubmitReqVO.java new file mode 100644 index 0000000..7c68b20 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/review/vo/RequirementReviewSubmitReqVO.java @@ -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 attendees; + + @Schema(description = "会议资料附件") + private List attachments; + + @Schema(description = "实际评审日期", example = "2026-05-20") + private LocalDate reviewTime; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java index 9dd97b8..ca584cd 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java @@ -94,7 +94,7 @@ public class ProductRequirementDO extends BaseDO { */ private String currentHandlerUserNickname; /** - * 默认实现项目ID,分流后可回填 + * 默认实现项目ID,指派后可回填 */ private Long implementProjectId; /** diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementStatusLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementStatusLogDO.java index 668b7f5..8da1076 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementStatusLogDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementStatusLogDO.java @@ -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; - } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/review/RequirementReviewAttendeeItem.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/review/RequirementReviewAttendeeItem.java new file mode 100644 index 0000000..26ab886 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/review/RequirementReviewAttendeeItem.java @@ -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; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/review/RequirementReviewDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/review/RequirementReviewDO.java new file mode 100644 index 0000000..d503c74 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/review/RequirementReviewDO.java @@ -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 attendees; + + @TableField(typeHandler = JacksonTypeHandler.class) + private List attachments; + + private LocalDateTime reviewTime; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementStatusLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementStatusLogMapper.java index b824d38..13bfdcf 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementStatusLogMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementStatusLogMapper.java @@ -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 selectListByRequirementIdsAndToStatuses(List requirementIds, + List toStatuses) { + if (requirementIds == null || requirementIds.isEmpty() + || toStatuses == null || toStatuses.isEmpty()) { + return Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .in(ProductRequirementStatusLogDO::getRequirementId, requirementIds) + .in(ProductRequirementStatusLogDO::getToStatus, toStatuses) + .orderByDesc(ProductRequirementStatusLogDO::getCreateTime) + .orderByDesc(ProductRequirementStatusLogDO::getId)); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/review/RequirementReviewMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/review/RequirementReviewMapper.java new file mode 100644 index 0000000..41b8f2e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/review/RequirementReviewMapper.java @@ -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 { + + default RequirementReviewDO selectByObjectTypeAndRequirementId(String objectType, Long requirementId) { + return selectOne(new LambdaQueryWrapperX() + .eq(RequirementReviewDO::getObjectType, objectType) + .eq(RequirementReviewDO::getRequirementId, requirementId)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java index 748bc72..c853edb 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java @@ -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 getRequirementPage(ProductRequirementPageReqVO pageReqVO); - /** - * 获取需求树形列表(分页) - * - * @param pageReqVO 分页请求 - * @return 分页结果(只按父需求分页,子需求不计入分页) - */ PageResult 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 getAllowedTransitions(Long requirementId, Long productId); - /** - * 批量获取需求当前可执行的状态动作列表 - * - * @param reqVO 批量查询请求 - * @return 按需求编号标识的可执行动作列表 - */ List getAllowedTransitionsBatch(ProductRequirementBatchReqVO reqVO); - /** - * 判断需求是否已分流并生成项目需求 - * - * @param requirementId 需求编号 - * @param productId 产品编号 - * @return 是否已分流 - */ boolean hasDispatchedProjectRequirement(Long requirementId, Long productId); - /** - * 批量判断需求是否已分流并生成项目需求 - * - * @param reqVO 批量查询请求 - * @return 按需求编号标识的分流状态 - */ List 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 getRequirementModuleTree(Long productId); - /** - * 获取需求所有状态字典列表 - * - * @return 状态字典列表 - */ List getRequirementStatusDict(); - /** - * 获取需求终止态状态字典列表 - * - * @return 终止态状态字典列表 - */ List getRequirementTerminalStatusDict(); - /** - * 获取产品需求分流后对应的项目需求跳转链接 - * - * @param productRequirementId 产品需求编号 - * @return 项目需求ID和关联项目ID - */ ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java index f0ade1a..0e80939 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java @@ -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 TERMINAL_STATUSES = List.of(STATUS_CLOSED, STATUS_REJECTED, STATUS_CANCELLED); - // 子需求允许大需求关闭的状态集合 - private static final List CHILD_ALLOW_CLOSE_STATUSES = List.of(STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED); // 允许删除的状态集合(实施中之前的状态) - private static final List ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH); + private static final List ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CLAIM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH); // 父需求取消时,子需求允许的状态集合(仅已拒绝和已取消) private static final List 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 requirements = requirementMapper.selectListByProductId(productId); + Map statusModelMap = getStatusModelMap(); + + ProductRequirementDashboardRespVO respVO = new ProductRequirementDashboardRespVO(); + respVO.setSummary(buildRequirementDashboardSummary(requirements)); + respVO.setRecentChanges(buildRequirementDashboardRecentChanges(productId, requirements, statusModelMap)); + return respVO; + } + + private ProductRequirementDashboardSummaryRespVO buildRequirementDashboardSummary(List 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 buildRequirementDashboardRecentChanges( + Long productId, List requirements, Map statusModelMap) { + List 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 statusModelMap, + List items) { + List 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 statusModelMap, + List 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 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 requirements, + Map statusModelMap, + List items) { + List requirementIds = requirements.stream() + .map(ProductRequirementDO::getId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + List terminalStatusCodes = statusModelMapper + .selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE); + List 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 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 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 allChildren = getAllRequirementsWithChildren(requirementId); + Set 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 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 projectRequirements = projectRequirementMapper.selectList( new LambdaQueryWrapperX() .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 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 } } + diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementService.java index 0619714..9cc0468 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementService.java @@ -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 getRequirementPage(ProjectRequirementPageReqVO pageReqVO); - /** - * 获取需求树形列表 - */ PageResult 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 getAllowedTransitions(Long requirementId, Long projectId); - /** - * 批量获取需求可执行动作列表 - */ List getAllowedTransitionsBatch(ProjectRequirementBatchReqVO reqVO); - /** - * 获取需求生命周期信息 - */ - ProjectRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long projectId); - - /** - * 创建需求模块 - */ Long createRequirementModule(ProjectRequirementModuleReqVO reqVO); - /** - * 更新需求模块 - */ void updateRequirementModule(ProjectRequirementModuleReqVO reqVO); - /** - * 删除需求模块 - */ void deleteRequirementModule(Long moduleId, Long projectId); - /** - * 获取需求模块树 - */ List getRequirementModuleTree(Long projectId); - /** - * 获取需求状态字典 - */ List getRequirementStatusDict(); - /** - * 获取需求终态字典 - */ List getRequirementTerminalStatusDict(); /** diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java index b2f51af..330bb02 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java @@ -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 TERMINAL_STATUSES = List.of( - STATUS_CLOSED, STATUS_REJECTED, STATUS_CANCELLED); - private static final List CHILD_ALLOW_CLOSE_STATUSES = List.of( - STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED); + private static final Set REVIEW_STATUS_CODES = Set.of( + STATUS_PENDING_REVIEW, STATUS_REVIEWED, STATUS_REVIEW_REJECTED); private static final List ALLOW_DELETE_STATUSES = List.of( - STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW); + STATUS_PENDING_CLAIM, STATUS_PENDING_REVIEW); private static final List 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 allChildren = getAllRequirementsWithChildren(requirementId); + Set 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 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 : ""; } -} \ No newline at end of file +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/review/RequirementReviewService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/review/RequirementReviewService.java new file mode 100644 index 0000000..4c780e1 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/review/RequirementReviewService.java @@ -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); + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/review/RequirementReviewServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/review/RequirementReviewServiceImpl.java new file mode 100644 index 0000000..99f313f --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/review/RequirementReviewServiceImpl.java @@ -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(); + } + +} diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java index d20f700..418eede 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java @@ -81,7 +81,7 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest { ArgumentCaptor 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); diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataRespVO.java index bc04828..943a1b6 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataRespVO.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataRespVO.java @@ -43,10 +43,6 @@ public class DictDataRespVO { @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") private String colorType; - @Schema(description = "标识", example = "system") - @ExcelProperty("标识") - private String sign; - @Schema(description = "css 样式", example = "btn-visible") private String cssClass; diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java index d317a12..cf8c6f8 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java @@ -42,10 +42,6 @@ public class DictDataSaveReqVO { @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") private String colorType; - @Schema(description = "标识", example = "system") - @Size(max = 255, message = "标识长度不能超过255个字符") - private String sign; - @Schema(description = "css 样式", example = "btn-visible") private String cssClass; diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java index 913f2e8..c8c6be9 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java @@ -22,9 +22,6 @@ public class DictDataSimpleRespVO { @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") private String colorType; - @Schema(description = "标识", example = "system") - private String sign; - @Schema(description = "css 样式", example = "btn-visible") private String cssClass; diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/app/dict/vo/AppDictDataRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/app/dict/vo/AppDictDataRespVO.java index 923f2c2..455b357 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/app/dict/vo/AppDictDataRespVO.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/app/dict/vo/AppDictDataRespVO.java @@ -23,7 +23,4 @@ public class AppDictDataRespVO { @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") private String dictType; - @Schema(description = "标识", example = "system") - private String sign; - } diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/dict/DictDataDO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/dict/DictDataDO.java index b796c86..2062cca 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/dict/DictDataDO.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/dict/DictDataDO.java @@ -52,11 +52,6 @@ public class DictDataDO extends BaseDO { * 对应到 element-ui 为 default、primary、success、info、warning、danger */ private String colorType; - /** - * 标识 - */ - @TableField(updateStrategy = FieldStrategy.ALWAYS) - private String sign; /** * css 样式 */