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 3a65626..a2a98b4 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 @@ -34,4 +34,19 @@ public interface ErrorCodeConstants { ErrorCode PRODUCT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_001_022, "产品状态已发生变化,请刷新后重试"); ErrorCode PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_001_023, "产品状态定义不存在或已停用"); + // ========== 产品需求 1-008-002-000 ========== + ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在"); + 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_CLOSE = new ErrorCode(1_008_002_005, "只有已验收的需求才能关闭"); + ErrorCode REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_006, "需求状态定义不存在或已停用"); + 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_MODULE_NOT_EXISTS = new ErrorCode(1_008_002_009, "需求模块不存在"); + ErrorCode REQUIREMENT_MODULE_NAME_DUPLICATE = new ErrorCode(1_008_002_010, "已经存在名称为【{}】的模块"); + ErrorCode REQUIREMENT_MODULE_NOT_BELONG_TO_PRODUCT = new ErrorCode(1_008_002_011, "模块不属于当前产品"); + ErrorCode REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS = new ErrorCode(1_008_002_012, "模块下存在非终态需求,不可删除"); + } 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 new file mode 100644 index 0000000..ce90146 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductRequirementController.java @@ -0,0 +1,162 @@ +package com.njcn.rdms.module.project.controller.admin.product; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementCloseReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementLifecycleRespVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementModuleRespVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementModuleSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementPageReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementRespVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementSplitReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementStatusTransitionRespVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementUpdateReqVO; +import com.njcn.rdms.module.project.service.product.ProductRequirementService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +/** + * 管理后台 - 产品需求控制器 + */ +@Tag(name = "管理后台 - 产品需求") +@RestController +@RequestMapping("/project/product/requirement") +@Validated +public class ProductRequirementController { + + @Resource + private ProductRequirementService requirementService; + + // ========== 需求管理 ========== + + @PostMapping("/create") + @Operation(summary = "创建产品需求") + public CommonResult createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) { + return success(requirementService.createRequirement(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新产品需求") + public CommonResult updateRequirement(@Valid @RequestBody ProductRequirementUpdateReqVO updateReqVO) { + requirementService.updateRequirement(updateReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取需求详情") + @Parameter(name = "id", description = "需求编号", required = true, example = "1024") + @Parameter(name = "productId", description = "产品编号", required = true, example = "1024") + public CommonResult getRequirement(@RequestParam("id") Long id, + @RequestParam("productId") Long productId) { + return success(requirementService.getRequirement(id)); + } + + @GetMapping("/page") + @Operation(summary = "获取需求分页列表") + public CommonResult> getRequirementPage(@Valid ProductRequirementPageReqVO pageReqVO) { + return success(requirementService.getRequirementPage(pageReqVO)); + } + + @GetMapping("/tree") + @Operation(summary = "获取需求树形列表") + @Parameter(name = "productId", description = "产品编号", required = true, example = "1024") + @Parameter(name = "moduleId", description = "模块编号", example = "1024") + public CommonResult> getRequirementTree(@RequestParam("productId") Long productId, + @RequestParam(value = "moduleId", required = false) Long moduleId) { + return success(requirementService.getRequirementTree(productId, moduleId)); + } + + @PostMapping("/change-status") + @Operation(summary = "变更需求状态") + public CommonResult changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) { + requirementService.changeRequirementStatus(reqVO); + return success(true); + } + + @PostMapping("/delete") + @Operation(summary = "删除产品需求") + @Parameter(name = "id", description = "需求编号", required = true, example = "1024") + @Parameter(name = "productId", description = "产品编号", required = true, example = "1024") + public CommonResult deleteRequirement(@RequestParam("id") Long id, + @RequestParam("productId") Long productId) { + requirementService.deleteRequirement(id, productId); + return success(true); + } + + @PostMapping("/split") + @Operation(summary = "拆分产品需求") + public CommonResult splitRequirement(@Valid @RequestBody ProductRequirementSplitReqVO reqVO) { + return success(requirementService.splitRequirement(reqVO)); + } + + @PostMapping("/close") + @Operation(summary = "关闭产品需求") + public CommonResult closeRequirement(@Valid @RequestBody ProductRequirementCloseReqVO reqVO) { + requirementService.closeRequirement(reqVO); + return success(true); + } + + @GetMapping("/allowed-transitions") + @Operation(summary = "获取需求可执行的状态动作列表") + @Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024") + @Parameter(name = "productId", description = "产品编号", required = true, example = "1024") + public CommonResult> getAllowedTransitions( + @RequestParam("requirementId") Long requirementId, + @RequestParam("productId") Long productId) { + return success(requirementService.getAllowedTransitions(requirementId, productId)); + } + + @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)); + } + + // ========== 模块管理 ========== + + @PostMapping("/module/create") + @Operation(summary = "创建需求模块") + public CommonResult createRequirementModule(@Valid @RequestBody ProductRequirementModuleSaveReqVO reqVO) { + return success(requirementService.createRequirementModule(reqVO)); + } + + @PutMapping("/module/update") + @Operation(summary = "更新需求模块") + public CommonResult updateRequirementModule(@Valid @RequestBody ProductRequirementModuleSaveReqVO reqVO) { + requirementService.updateRequirementModule(reqVO); + return success(true); + } + + @PostMapping("/module/delete") + @Operation(summary = "删除需求模块") + @Parameter(name = "moduleId", description = "模块编号", required = true, example = "1024") + @Parameter(name = "productId", description = "产品编号", required = true, example = "1024") + public CommonResult deleteRequirementModule(@RequestParam("moduleId") Long moduleId, + @RequestParam("productId") Long productId) { + requirementService.deleteRequirementModule(moduleId, productId); + return success(true); + } + + @GetMapping("/module/tree") + @Operation(summary = "获取需求模块树") + @Parameter(name = "productId", description = "产品编号", required = true, example = "1024") + public CommonResult> getRequirementModuleTree(@RequestParam("productId") Long productId) { + return success(requirementService.getRequirementModuleTree(productId)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementCloseReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementCloseReqVO.java new file mode 100644 index 0000000..628dbe2 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementCloseReqVO.java @@ -0,0 +1,29 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 产品需求关闭 Request VO + */ +@Schema(description = "管理后台 - 产品需求关闭 Request VO") +@Data +public class ProductRequirementCloseReqVO { + + @Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "需求编号不能为空") + private Long id; + + @Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品ID不能为空") + private Long productId; + + @Schema(description = "关闭原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "需求已完成验收") + @NotBlank(message = "关闭原因不能为空") + @Size(max = 255, message = "关闭原因长度不能超过255个字符") + private String reason; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/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 new file mode 100644 index 0000000..e8e2bab --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementLifecycleRespVO.java @@ -0,0 +1,33 @@ +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/ProductRequirementModuleRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementModuleRespVO.java new file mode 100644 index 0000000..f650939 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementModuleRespVO.java @@ -0,0 +1,39 @@ +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 ProductRequirementModuleRespVO { + + @Schema(description = "模块ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "父模块ID(0表示顶级)", example = "0") + private Long parentId; + + @Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long productId; + + @Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能") + private String moduleName; + + @Schema(description = "模块说明", example = "产品核心功能模块") + private String remark; + + @Schema(description = "图标", example = "icon-function") + private String icon; + + @Schema(description = "排序值", example = "0") + private Integer sort; + + @Schema(description = "子模块列表") + private List children; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementModuleSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementModuleSaveReqVO.java new file mode 100644 index 0000000..b4c6858 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementModuleSaveReqVO.java @@ -0,0 +1,40 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 产品需求模块保存 Request VO + */ +@Schema(description = "管理后台 - 产品需求模块保存 Request VO") +@Data +public class ProductRequirementModuleSaveReqVO { + + @Schema(description = "模块ID(编辑时传入)", example = "1024") + private Long id; + + @Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品ID不能为空") + private Long productId; + + @Schema(description = "父模块ID(0表示顶级)", example = "0") + private Long parentId; + + @Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能") + @NotBlank(message = "模块名称不能为空") + @Size(max = 100, message = "模块名称长度不能超过100个字符") + private String moduleName; + + @Schema(description = "模块说明", example = "产品核心功能模块") + private String remark; + + @Schema(description = "图标", example = "icon-function") + private String icon; + + @Schema(description = "排序值", example = "0") + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementPageReqVO.java new file mode 100644 index 0000000..a4d4b35 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementPageReqVO.java @@ -0,0 +1,43 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import com.njcn.rdms.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 管理后台 - 产品需求分页 Request VO + */ +@Schema(description = "管理后台 - 产品需求分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProductRequirementPageReqVO extends PageParam { + + @Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long productId; + + @Schema(description = "所属模块ID", example = "1024") + private Long moduleId; + + @Schema(description = "父需求ID(查询子需求时使用)", example = "1024") + private Long parentId; + + @Schema(description = "标题关键词", example = "模块") + private String title; + + @Schema(description = "需求分类字典值", example = "function") + private String category; + + @Schema(description = "优先级(0低 1中 2高 3紧急)", example = "1") + private Integer priority; + + @Schema(description = "状态编码", example = "pending_dispatch") + private String statusCode; + + @Schema(description = "当前处理人用户编号", example = "1024") + private Long currentHandlerUserId; + + @Schema(description = "来源类型(manual:手工新增, work_order:工单流转)", example = "manual") + private String sourceType; + +} 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 new file mode 100644 index 0000000..46a32c9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementRespVO.java @@ -0,0 +1,97 @@ +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; +import java.util.List; + +/** + * 管理后台 - 产品需求 Response VO + */ +@Schema(description = "管理后台 - 产品需求 Response VO") +@Data +public class ProductRequirementRespVO { + + @Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "父需求ID(0表示顶级需求)", example = "0") + private Long parentId; + + @Schema(description = "所属模块ID", example = "1024") + private Long moduleId; + + @Schema(description = "是否需要评审(0不需要;1需要)", example = "0") + private Integer reviewRequired; + + @Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理") + private String title; + + @Schema(description = "需求描述(富文本)", example = "

详细描述需求内容

") + private String description; + + @Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function") + private String category; + + @Schema(description = "需求分类名称", example = "功能需求") + private String categoryName; + + @Schema(description = "来源类型(manual:手工新增, work_order:工单流转)", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual") + private String sourceType; + + @Schema(description = "来源业务ID", example = "1024") + private Long sourceBizId; + + @Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer priority; + + @Schema(description = "优先级名称", example = "中") + private String priorityName; + + @Schema(description = "当前状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending_dispatch") + private String statusCode; + + @Schema(description = "当前状态名称", example = "待分流") + private String statusName; + + @Schema(description = "最近一次状态动作原因", example = "评审通过") + private String lastStatusReason; + + @Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long proposerId; + + @Schema(description = "提出人用户姓名", example = "张三") + private String proposerNickname; + + @Schema(description = "当前处理人用户编号", example = "1024") + private Long currentHandlerUserId; + + @Schema(description = "当前处理人姓名", example = "李四") + private String currentHandlerUserNickname; + + @Schema(description = "默认实现项目编号", example = "1024") + private Long implementProjectId; + + @Schema(description = "实现项目名称", example = "NPQS-10086") + private String implementProjectName; + + @Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime completionDate; + + @Schema(description = "排序值", example = "0") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "子需求列表(树形结构)") + private List children; + + @Schema(description = "是否为终态(已拒绝、已取消、已关闭)", example = "false") + private Boolean terminal; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSaveReqVO.java new file mode 100644 index 0000000..cc2798b --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSaveReqVO.java @@ -0,0 +1,66 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 管理后台 - 产品需求保存 Request VO + */ +@Schema(description = "管理后台 - 产品需求保存 Request VO") +@Data +public class ProductRequirementSaveReqVO { + + @Schema(description = "需求ID", example = "1024") + private Long id; + + @Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private Long productId; + + @Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024") + private Long moduleId; + + @Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "是否需要评审不能为空") + private Integer reviewRequired; + + @Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理") + @NotBlank(message = "需求标题不能为空") + @Size(max = 200, message = "需求标题长度不能超过200个字符") + private String title; + + @Schema(description = "需求描述(富文本)", example = "

详细描述需求内容

") + private String description; + + @Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function") + @NotBlank(message = "需求分类不能为空") + @Size(max = 64, message = "需求分类长度不能超过64个字符") + private String category; + + @Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优先级不能为空") + private Integer priority; + + @Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "提出人不能为空") + private Long proposerId; + + @Schema(description = "当前处理人用户编号", example = "1024") + private Long currentHandlerUserId; + + @Schema(description = "默认实现项目编号", example = "1024") + private Long implementProjectId; + + @Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "预期完成时间不能为空") + private LocalDateTime completionDate; + + @Schema(description = "排序值(越小越靠前)", example = "0") + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSplitReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSplitReqVO.java new file mode 100644 index 0000000..bc39b68 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementSplitReqVO.java @@ -0,0 +1,59 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 管理后台 - 产品需求拆分 Request VO + */ +@Schema(description = "管理后台 - 产品需求拆分 Request VO") +@Data +public class ProductRequirementSplitReqVO { + + @Schema(description = "父需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "父需求编号不能为空") + private Long parentId; + + @Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private Long productId; + + @Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理") + @NotBlank(message = "需求标题不能为空") + @Size(max = 200, message = "需求标题长度不能超过200个字符") + private String title; + + @Schema(description = "需求描述(富文本)", example = "

详细描述需求内容

") + private String description; + + @Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function") + @NotBlank(message = "需求分类不能为空") + @Size(max = 64, message = "需求分类长度不能超过64个字符") + private String category; + + @Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优先级不能为空") + private Integer priority; + + @Schema(description = "当前处理人用户编号", example = "1024") + private Long currentHandlerUserId; + + @Schema(description = "默认实现项目编号", example = "1024") + private Long implementProjectId; + + @Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "预期完成时间不能为空") + private LocalDateTime completionDate; + + @Schema(description = "排序值(越小越靠前)", example = "0") + private Integer sort; + + @Schema(description = "拆分原因", example = "需求过大,需要拆分实现") + private String splitReason; + +} 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 new file mode 100644 index 0000000..09cc935 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusActionReqVO.java @@ -0,0 +1,32 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 管理后台 - 产品需求状态变更 Request VO + * 注意:需求不直接关联产品,通过模块间接关联 + */ +@Schema(description = "管理后台 - 产品需求状态变更 Request VO") +@Data +public class ProductRequirementStatusActionReqVO { + + @Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "需求ID不能为空") + private Long id; + + @Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "dispatch") + @NotBlank(message = "动作编码不能为空") + @Size(max = 32, message = "动作编码长度不能超过32个字符") + private String actionCode; + + @Schema(description = "状态变更原因", example = "评审通过,进入分流阶段") + private String reason; + + @Schema(description = "实现项目编号(dispatch动作时可选)", example = "1024") + private Long implementProjectId; + +} 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 new file mode 100644 index 0000000..5dbf02a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementStatusTransitionRespVO.java @@ -0,0 +1,30 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 管理后台 - 产品需求状态可执行动作 Response VO + */ +@Schema(description = "管理后台 - 产品需求状态可执行动作 Response VO") +@Data +@NoArgsConstructor +public class ProductRequirementStatusTransitionRespVO { + + @Schema(description = "动作编码", example = "dispatch") + private String actionCode; + + @Schema(description = "动作名称", example = "明确分流/拆分") + private String actionName; + + @Schema(description = "目标状态编码", example = "implementing") + private String toStatusCode; + + @Schema(description = "目标状态名称", example = "实施中") + private String toStatusName; + + @Schema(description = "是否必须填写原因", example = "false") + private Boolean needReason; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementUpdateReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementUpdateReqVO.java new file mode 100644 index 0000000..49f5e27 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/requirement/ProductRequirementUpdateReqVO.java @@ -0,0 +1,67 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.requirement; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 管理后台 - 产品需求编辑 Request VO + */ +@Schema(description = "管理后台 - 产品需求编辑 Request VO") +@Data +public class ProductRequirementUpdateReqVO { + + @Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "需求ID不能为空") + private Long id; + + @Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品ID不能为空") + private Long productId; + + @Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024") + private Long moduleId; + + @Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "是否需要评审不能为空") + private Integer reviewRequired; + + @Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理") + @NotBlank(message = "需求标题不能为空") + @Size(max = 200, message = "需求标题长度不能超过200个字符") + private String title; + + @Schema(description = "需求描述(富文本)", example = "

详细描述需求内容

") + private String description; + + @Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function") + @NotBlank(message = "需求分类不能为空") + @Size(max = 64, message = "需求分类长度不能超过64个字符") + private String category; + + @Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优先级不能为空") + private Integer priority; + + @Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "提出人不能为空") + private Long proposerId; + + @Schema(description = "当前处理人用户编号", example = "1024") + private Long currentHandlerUserId; + + @Schema(description = "默认实现项目编号", example = "1024") + private Long implementProjectId; + + @Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "预期完成时间不能为空") + private LocalDateTime completionDate; + + @Schema(description = "排序值(越小越靠前)", example = "0") + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java new file mode 100644 index 0000000..e81795c --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementDO.java @@ -0,0 +1,101 @@ +package com.njcn.rdms.module.project.dal.dataobject.product; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 产品需求主表 + */ +@TableName("rdms_product_requirement") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProductRequirementDO extends BaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + /** + * 父需求ID(0表示顶级需求) + */ + private Long parentId; + /** + * 所属模块ID(0表示全部需求) + */ + private Long moduleId; + /** + * 所属产品ID + */ + private Long productId; + /** + * 是否需要评审(0不需要;1需要) + */ + private Integer reviewRequired; + /** + * 需求标题 + */ + private String title; + /** + * 需求描述(富文本) + */ + private String description; + /** + * 需求分类字典值 + */ + private String category; + /** + * 来源类型(manual:手工新增, work_order:工单流转) + */ + private String sourceType; + /** + * 来源业务ID + */ + private Long sourceBizId; + /** + * 优先级(0低 1中 2高 3紧急) + */ + private Integer priority; + /** + * 当前状态编码 + */ + private String statusCode; + /** + * 最近一次状态动作原因 + */ + private String lastStatusReason; + /** + * 提出人用户ID + */ + private Long proposerId; + /** + * 提出人用户姓名快照 + */ + private String proposerNickname; + /** + * 当前处理人用户ID + */ + private Long currentHandlerUserId; + /** + * 当前处理人姓名快照 + */ + private String currentHandlerUserNickname; + /** + * 默认实现项目ID(分流后填写) + */ + private Long implementProjectId; + /** + * 预期完成时间 + */ + private LocalDateTime completionDate; + /** + * 排序值(越小越靠前) + */ + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementModuleDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementModuleDO.java new file mode 100644 index 0000000..ebff2ce --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementModuleDO.java @@ -0,0 +1,47 @@ +package com.njcn.rdms.module.project.dal.dataobject.product; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 产品需求模块表(支撑前端左侧模块树) + */ +@TableName("rdms_product_requirement_module") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProductRequirementModuleDO extends BaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + /** + * 父模块ID(0表示顶级) + */ + private Long parentId; + /** + * 所属产品ID + */ + private Long productId; + /** + * 模块名称 + */ + private String moduleName; + /** + * 模块说明 + */ + private String remark; + /** + * 图标 + */ + private String icon; + /** + * 排序值 + */ + private Integer sort; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementStatusLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementStatusLogDO.java new file mode 100644 index 0000000..668b7f5 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductRequirementStatusLogDO.java @@ -0,0 +1,59 @@ +package com.njcn.rdms.module.project.dal.dataobject.product; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 产品需求状态变更日志表 + */ +@TableName("rdms_product_requirement_status_log") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProductRequirementStatusLogDO extends BaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + /** + * 需求ID + */ + private Long requirementId; + /** + * 动作编码 + */ + private String actionType; + /** + * 变更前状态 + */ + private String fromStatus; + /** + * 变更后状态 + */ + private String toStatus; + /** + * 动作原因 + */ + private String reason; + /** + * 操作人ID + */ + private Long operatorUserId; + /** + * 操作人名称快照 + */ + private String operatorName; + /** + * 需求标题快照 + */ + private String requirementTitleSnapshot; + /** + * 备注 + */ + private String remark; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementMapper.java new file mode 100644 index 0000000..6fe9f75 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementMapper.java @@ -0,0 +1,110 @@ +package com.njcn.rdms.module.project.dal.mysql.product; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementPageReqVO; +import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * 产品需求 Mapper + */ +@Mapper +public interface ProductRequirementMapper extends BaseMapperX { + + /** + * 分页查询需求列表 + */ + default PageResult selectPage(ProductRequirementPageReqVO reqVO) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + // 标题模糊搜索 + if (StringUtils.hasText(reqVO.getTitle())) { + queryWrapper.like(ProductRequirementDO::getTitle, reqVO.getTitle()); + } + // 分类精确匹配 + queryWrapper.eqIfPresent(ProductRequirementDO::getCategory, reqVO.getCategory()) + // 优先级精确匹配 + .eqIfPresent(ProductRequirementDO::getPriority, reqVO.getPriority()) + // 状态精确匹配 + .eqIfPresent(ProductRequirementDO::getStatusCode, reqVO.getStatusCode()) + // 负责人精确匹配 + .eqIfPresent(ProductRequirementDO::getCurrentHandlerUserId, reqVO.getCurrentHandlerUserId()) + // 来源类型精确匹配 + .eqIfPresent(ProductRequirementDO::getSourceType, reqVO.getSourceType()) + // 模块ID精确匹配 + .eqIfPresent(ProductRequirementDO::getModuleId, reqVO.getModuleId()) + // 父需求ID精确匹配(查询子需求时使用) + .eqIfPresent(ProductRequirementDO::getParentId, reqVO.getParentId()) + // 产品ID精确匹配 + .eq(ProductRequirementDO::getProductId, reqVO.getProductId()) + // 按排序值升序,再按创建时间降序 + .orderByAsc(ProductRequirementDO::getSort) + .orderByDesc(ProductRequirementDO::getCreateTime); + return selectPage(reqVO, queryWrapper); + } + + /** + * 根据产品ID和模块ID查询需求列表 + */ + default List selectListByProductIdAndModuleId(Long productId, Long moduleId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProductRequirementDO::getProductId, productId) + .eq(ProductRequirementDO::getModuleId, moduleId) + .orderByAsc(ProductRequirementDO::getSort) + .orderByDesc(ProductRequirementDO::getCreateTime)); + } + + /** + * 根据父需求ID查询子需求列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProductRequirementDO::getParentId, parentId) + .orderByAsc(ProductRequirementDO::getSort) + .orderByDesc(ProductRequirementDO::getCreateTime)); + } + + /** + * 根据产品ID查询所有需求(用于模块删除时级联处理) + */ + default List selectListByProductId(Long productId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProductRequirementDO::getProductId, productId) + .orderByAsc(ProductRequirementDO::getSort)); + } + + /** + * 根据模块ID查询需求列表 + */ + default List selectListByModuleId(Long moduleId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProductRequirementDO::getModuleId, moduleId) + .orderByAsc(ProductRequirementDO::getSort)); + } + + /** + * 带并发控制的状态更新(id + fromStatus 条件更新) + */ + default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) { + ProductRequirementDO update = new ProductRequirementDO(); + update.setStatusCode(toStatus); + update.setLastStatusReason(lastStatusReason); + return update(update, new LambdaQueryWrapperX() + .eq(ProductRequirementDO::getId, id) + .eq(ProductRequirementDO::getStatusCode, fromStatus)); + } + + /** + * 根据ID和状态删除(带并发控制) + */ + default int deleteByIdAndStatus(Long id, String statusCode) { + return delete(new LambdaQueryWrapperX() + .eq(ProductRequirementDO::getId, id) + .eq(ProductRequirementDO::getStatusCode, statusCode)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementModuleMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementModuleMapper.java new file mode 100644 index 0000000..9c13219 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementModuleMapper.java @@ -0,0 +1,53 @@ +package com.njcn.rdms.module.project.dal.mysql.product; + +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.product.ProductRequirementModuleDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 产品需求模块 Mapper + */ +@Mapper +public interface ProductRequirementModuleMapper extends BaseMapperX { + + /** + * 根据产品ID查询模块列表(用于构建模块树) + */ + default List selectListByProductId(Long productId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProductRequirementModuleDO::getProductId, productId) + .orderByAsc(ProductRequirementModuleDO::getSort) + .orderByAsc(ProductRequirementModuleDO::getCreateTime)); + } + + /** + * 根据父模块ID查询子模块列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProductRequirementModuleDO::getParentId, parentId) + .orderByAsc(ProductRequirementModuleDO::getSort)); + } + + /** + * 根据产品ID和模块名称查询模块(用于校验名称唯一性) + */ + default ProductRequirementModuleDO selectByProductIdAndModuleName(Long productId, String moduleName) { + return selectOne(new LambdaQueryWrapperX() + .eq(ProductRequirementModuleDO::getProductId, productId) + .eq(ProductRequirementModuleDO::getModuleName, moduleName)); + } + + /** + * 根据产品ID和父模块ID查询模块(用于查找产品的"全部需求"根模块) + */ + default ProductRequirementModuleDO selectByProductIdAndParentId(Long productId, Long parentId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ProductRequirementModuleDO::getProductId, productId) + .eq(ProductRequirementModuleDO::getParentId, parentId)); + } + +} 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 new file mode 100644 index 0000000..b824d38 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductRequirementStatusLogMapper.java @@ -0,0 +1,25 @@ +package com.njcn.rdms.module.project.dal.mysql.product; + +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.product.ProductRequirementStatusLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 产品需求状态变更日志 Mapper + */ +@Mapper +public interface ProductRequirementStatusLogMapper extends BaseMapperX { + + /** + * 根据需求ID查询状态变更日志列表 + */ + default List selectListByRequirementId(Long requirementId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProductRequirementStatusLogDO::getRequirementId, requirementId) + .orderByDesc(ProductRequirementStatusLogDO::getCreateTime)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java new file mode 100644 index 0000000..9702ded --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementService.java @@ -0,0 +1,134 @@ +package com.njcn.rdms.module.project.service.product; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*; + +import java.util.List; + +/** + * 产品需求 Service 接口 + */ +public interface ProductRequirementService { + + /** + * 创建产品需求 + * + * @param createReqVO 创建请求 + * @return 需求编号 + */ + Long createRequirement(ProductRequirementSaveReqVO createReqVO); + + /** + * 更新产品需求(不含状态变更) + * + * @param updateReqVO 更新请求 + */ + void updateRequirement(ProductRequirementUpdateReqVO updateReqVO); + + /** + * 获取需求详情 + * + * @param id 需求编号 + * @return 需求详情 + */ + ProductRequirementRespVO getRequirement(Long id); + + /** + * 获取需求分页列表 + * + * @param pageReqVO 分页请求 + * @return 分页结果 + */ + PageResult getRequirementPage(ProductRequirementPageReqVO pageReqVO); + + /** + * 获取需求树形列表(包含子需求) + * + * @param productId 产品编号 + * @param moduleId 模块编号(可为null) + * @return 需求树形列表 + */ + List getRequirementTree(Long productId, Long moduleId); + + /** + * 变更需求状态 + * + * @param reqVO 状态动作请求 + */ + void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO); + + /** + * 删除需求 + * + * @param id 需求编号 + * @param productId 产品编号 + */ + 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 requirementId 需求编号 + * @param productId 产品编号 + * @return 生命周期信息 + */ + ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId); + + // ========== 模块管理 ========== + + /** + * 创建需求模块 + * + * @param reqVO 模块保存请求 + * @return 模块编号 + */ + Long createRequirementModule(ProductRequirementModuleSaveReqVO reqVO); + + /** + * 更新需求模块 + * + * @param reqVO 模块保存请求 + */ + void updateRequirementModule(ProductRequirementModuleSaveReqVO reqVO); + + /** + * 删除需求模块(级联删除模块下需求) + * + * @param moduleId 模块编号 + * @param productId 产品编号 + */ + void deleteRequirementModule(Long moduleId, Long productId); + + /** + * 获取产品需求模块树 + * + * @param productId 产品编号 + * @return 模块树 + */ + List getRequirementModuleTree(Long productId); + +} 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 new file mode 100644 index 0000000..fdfcef7 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java @@ -0,0 +1,710 @@ +package com.njcn.rdms.module.project.service.product; + +import com.google.common.annotations.VisibleForTesting; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*; +import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO; +import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO; +import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO; +import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO; +import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper; +import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper; +import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper; +import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; +import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +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 ProductRequirementServiceImpl implements ProductRequirementService { + + // 对象类型常量 + private static final String REQUIREMENT_OBJECT_TYPE = "product_requirement"; + private static final String PRODUCT_OBJECT_TYPE = "product"; + + // 需求状态常量 + private static final String STATUS_PENDING_CONFIRM = "pending_confirm"; + private static final String STATUS_PENDING_REVIEW = "pending_review"; + private static final String STATUS_PENDING_DISPATCH = "pending_dispatch"; + 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 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 String PRODUCT_QUERY_PERMISSION = "project:product:query"; + private static final String PRODUCT_UPDATE_PERMISSION = "project:product:update"; + + // 审计动作常量 + private static final String ACTION_CREATE = "create"; + private static final String ACTION_UPDATE = "update"; + private static final String ACTION_DELETE = "delete"; + private static final String ACTION_SPLIT = "split"; + private static final String ACTION_CLOSE = "close"; + private static final String BIZ_TYPE_REQUIREMENT = "product_requirement"; + + @Resource + private ProductRequirementMapper requirementMapper; + @Resource + private ProductRequirementModuleMapper moduleMapper; + @Resource + private ProductRequirementStatusLogMapper statusLogMapper; + @Resource + private BizAuditLogMapper bizAuditLogMapper; + @Resource + private ObjectStatusTransitionMapper statusTransitionMapper; + @Resource + private ObjectStatusModelMapper statusModelMapper; + + // ========== 需求增删改查 ========== + + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#createReqVO.productId", + permission = PRODUCT_UPDATE_PERMISSION) + public Long createRequirement(ProductRequirementSaveReqVO createReqVO) { + // 当未选择模块时,自动归属到该产品的"全部需求"模块 + Long moduleId = resolveModuleId(createReqVO.getModuleId(), createReqVO.getProductId()); + // 校验模块是否属于当前产品 + validateModuleBelongsToProduct(moduleId, createReqVO.getProductId()); + + ProductRequirementDO requirement = new ProductRequirementDO(); + requirement.setProductId(createReqVO.getProductId()); + requirement.setParentId(0L); // 新增需求默认是顶级需求 + requirement.setModuleId(moduleId); + requirement.setReviewRequired(createReqVO.getReviewRequired()); + requirement.setTitle(createReqVO.getTitle().trim()); + requirement.setDescription(normalizeNullableText(createReqVO.getDescription())); + requirement.setCategory(createReqVO.getCategory()); + requirement.setSourceType("manual"); // 手工新增默认来源类型 + requirement.setPriority(createReqVO.getPriority()); + // 根据是否需要评审确定初始状态 + String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1) + ? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH; + requirement.setStatusCode(initialStatus); + requirement.setProposerId(createReqVO.getProposerId()); + requirement.setCurrentHandlerUserId(createReqVO.getCurrentHandlerUserId()); + requirement.setImplementProjectId(createReqVO.getImplementProjectId()); + requirement.setCompletionDate(createReqVO.getCompletionDate()); + requirement.setSort(createReqVO.getSort() != null ? createReqVO.getSort() : 0); + requirementMapper.insert(requirement); + + // 写入业务审计日志 + writeBizAuditLog(requirement, ACTION_CREATE, null, initialStatus, null, null); + return requirement.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#updateReqVO.productId", + permission = PRODUCT_UPDATE_PERMISSION) + public void updateRequirement(ProductRequirementUpdateReqVO updateReqVO) { + ProductRequirementDO requirement = validateRequirementExists(updateReqVO.getId()); + // 校验终态不允许编辑 + validateRequirementEditable(requirement); + // 当未选择模块时,自动归属到该产品的"全部需求"模块 + Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProductId()); + // 校验模块是否属于当前产品 + validateModuleBelongsToProduct(moduleId, updateReqVO.getProductId()); + + String fromStatus = requirement.getStatusCode(); + requirement.setModuleId(moduleId); + requirement.setReviewRequired(updateReqVO.getReviewRequired()); + requirement.setTitle(updateReqVO.getTitle().trim()); + requirement.setDescription(normalizeNullableText(updateReqVO.getDescription())); + requirement.setCategory(updateReqVO.getCategory()); + requirement.setPriority(updateReqVO.getPriority()); + requirement.setProposerId(updateReqVO.getProposerId()); + requirement.setCurrentHandlerUserId(updateReqVO.getCurrentHandlerUserId()); + requirement.setImplementProjectId(updateReqVO.getImplementProjectId()); + requirement.setCompletionDate(updateReqVO.getCompletionDate()); + requirement.setSort(updateReqVO.getSort() != null ? updateReqVO.getSort() : 0); + requirementMapper.updateById(requirement); + + writeBizAuditLog(requirement, ACTION_UPDATE, fromStatus, requirement.getStatusCode(), null, null); + } + + @Override + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", + permission = PRODUCT_QUERY_PERMISSION) + public ProductRequirementRespVO getRequirement(Long id) { + ProductRequirementDO requirement = validateRequirementExists(id); + return buildRequirementRespVO(requirement); + } + + @Override + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#pageReqVO.productId", + permission = PRODUCT_QUERY_PERMISSION) + public PageResult getRequirementPage(ProductRequirementPageReqVO pageReqVO) { + // 判断是否查询"全部需求"模块:通过查询模块的parentId是否为0L来判断 + if (pageReqVO.getModuleId() != null && isAllRequirementsModule(pageReqVO.getModuleId())) { + // "全部需求"模块,忽略模块ID条件,查询该产品下所有需求 + pageReqVO.setModuleId(null); + } + PageResult pageResult = requirementMapper.selectPage(pageReqVO); + List list = pageResult.getList().stream() + .map(this::buildRequirementRespVO) + .collect(Collectors.toList()); + return new PageResult<>(list, pageResult.getTotal()); + } + + @Override + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", + permission = PRODUCT_QUERY_PERMISSION) + public List getRequirementTree(Long productId, Long moduleId) { + // 查询当前产品下的所有顶级需求 + ProductRequirementPageReqVO pageReqVO = new ProductRequirementPageReqVO(); + pageReqVO.setProductId(productId); + pageReqVO.setParentId(0L); + pageReqVO.setPageSize(1000); // 树形查询取较大值 + + // 判断是否查询"全部需求"模块:通过查询模块的parentId是否为0L来判断 + if (moduleId != null && !isAllRequirementsModule(moduleId)) { + // 非"全部需求"模块,添加模块ID过滤条件 + pageReqVO.setModuleId(moduleId); + } + // 如果是"全部需求"模块,不设置moduleId条件,查询该产品下所有需求 + + List parentList = requirementMapper.selectPage(pageReqVO).getList(); + + // 构建树形结构 + return parentList.stream() + .map(this::buildRequirementRespVOWithChildren) + .collect(Collectors.toList()); + } + + /** + * 判断指定模块是否为"全部需求"模块(parentId = 0L 的根模块) + */ + @VisibleForTesting + boolean isAllRequirementsModule(Long moduleId) { + if (moduleId == null) { + return false; + } + ProductRequirementModuleDO module = moduleMapper.selectById(moduleId); + return module != null && module.getParentId() != null && module.getParentId() == 0L; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId", + permission = PRODUCT_UPDATE_PERMISSION) + public void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) { + ProductRequirementDO requirement = validateRequirementExists(reqVO.getId()); + String actionCode = reqVO.getActionCode().trim(); + String fromStatus = requirement.getStatusCode(); + + // 校验状态流转是否合法 + ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode); + String reason = normalizeNullableText(reqVO.getReason()); + // 校验是否需要填写原因 + validateTransitionReason(transition, reason); + + String toStatus = transition.getToStatusCode(); + + // dispatch动作时更新实现项目 + if ("dispatch".equals(actionCode) && reqVO.getImplementProjectId() != null) { + requirement.setImplementProjectId(reqVO.getImplementProjectId()); + } + + // 带并发控制的状态更新 + int updateCount = requirementMapper.updateStatusByIdAndStatus(requirement.getId(), fromStatus, toStatus, reason); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED); + } + requirement.setStatusCode(toStatus); + requirement.setLastStatusReason(reason); + + // 写入状态变更日志 + writeRequirementStatusLog(requirement, actionCode, fromStatus, toStatus, reason); + // 写入业务审计日志 + writeBizAuditLog(requirement, actionCode, fromStatus, toStatus, null, reason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", + permission = PRODUCT_UPDATE_PERMISSION) + public void deleteRequirement(Long id, Long productId) { + ProductRequirementDO requirement = validateRequirementExists(id); + String fromStatus = requirement.getStatusCode(); + + // 带并发控制的删除(以当前状态作为条件) + int deleteCount = requirementMapper.deleteByIdAndStatus(id, fromStatus); + if (deleteCount != 1) { + throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED); + } + + writeBizAuditLog(requirement, ACTION_DELETE, fromStatus, null, null, null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId", + permission = PRODUCT_UPDATE_PERMISSION) + public Long splitRequirement(ProductRequirementSplitReqVO reqVO) { + // 校验父需求是否存在 + ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId()); + // 校验父需求状态是否允许拆分(只能是待分流或实施中) + validateParentAllowSplit(parentRequirement); + + // 创建子需求 + ProductRequirementDO childRequirement = new ProductRequirementDO(); + childRequirement.setParentId(reqVO.getParentId()); + childRequirement.setModuleId(parentRequirement.getModuleId()); // 子需求继承父需求的模块 + childRequirement.setReviewRequired(0); // 子需求默认不需要评审 + childRequirement.setTitle(reqVO.getTitle().trim()); + childRequirement.setDescription(normalizeNullableText(reqVO.getDescription())); + childRequirement.setCategory(reqVO.getCategory()); + childRequirement.setSourceType(parentRequirement.getSourceType()); // 继承父需求来源类型 + childRequirement.setPriority(reqVO.getPriority()); + // 子需求初始状态为待分流 + childRequirement.setStatusCode(STATUS_PENDING_DISPATCH); + childRequirement.setProposerId(parentRequirement.getProposerId()); // 继承父需求提出人 + childRequirement.setCurrentHandlerUserId(reqVO.getCurrentHandlerUserId()); + childRequirement.setImplementProjectId(reqVO.getImplementProjectId()); + childRequirement.setCompletionDate(reqVO.getCompletionDate()); + childRequirement.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0); + requirementMapper.insert(childRequirement); + + // 父需求状态从待分流变为实施中 + if (STATUS_PENDING_DISPATCH.equals(parentRequirement.getStatusCode())) { + String fromStatus = parentRequirement.getStatusCode(); + int updateCount = requirementMapper.updateStatusByIdAndStatus( + parentRequirement.getId(), fromStatus, STATUS_IMPLEMENTING, null); + if (updateCount == 1) { + parentRequirement.setStatusCode(STATUS_IMPLEMENTING); + writeRequirementStatusLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null); + writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null, null); + } + } + + // 写入子需求的业务审计日志 + writeBizAuditLog(childRequirement, ACTION_CREATE, null, STATUS_PENDING_DISPATCH, null, null); + return childRequirement.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId", + permission = PRODUCT_UPDATE_PERMISSION) + public void closeRequirement(ProductRequirementCloseReqVO reqVO) { + ProductRequirementDO requirement = validateRequirementExists(reqVO.getId()); + String fromStatus = requirement.getStatusCode(); + String reason = reqVO.getReason().trim(); + + // 校验当前状态是否为已验收(只有已验收才能关闭) + if (!STATUS_ACCEPTED.equals(fromStatus)) { + throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_CLOSE); + } + + // 如果是父需求,校验所有子需求是否允许关闭 + List children = requirementMapper.selectListByParentId(requirement.getId()); + if (!children.isEmpty()) { + for (ProductRequirementDO child : children) { + if (!CHILD_ALLOW_CLOSE_STATUSES.contains(child.getStatusCode())) { + throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE); + } + } + // 将所有已验收的子需求关闭 + for (ProductRequirementDO child : children) { + if (STATUS_ACCEPTED.equals(child.getStatusCode())) { + int updateCount = requirementMapper.updateStatusByIdAndStatus( + child.getId(), STATUS_ACCEPTED, STATUS_CLOSED, reason); + if (updateCount == 1) { + writeRequirementStatusLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, reason); + writeBizAuditLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, null, reason); + } + } + } + } + + // 关闭当前需求 + int updateCount = requirementMapper.updateStatusByIdAndStatus( + requirement.getId(), fromStatus, STATUS_CLOSED, reason); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED); + } + requirement.setStatusCode(STATUS_CLOSED); + requirement.setLastStatusReason(reason); + + writeRequirementStatusLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, reason); + writeBizAuditLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, null, reason); + } + + @Override + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", + permission = PRODUCT_QUERY_PERMISSION) + public List getAllowedTransitions(Long requirementId, Long productId) { + ProductRequirementDO requirement = validateRequirementExists(requirementId); + String currentStatus = requirement.getStatusCode(); + + // 查询当前状态允许的所有流转 + List transitions = statusTransitionMapper + .selectListByObjectTypeAndFromStatus(REQUIREMENT_OBJECT_TYPE, currentStatus); + + return transitions.stream().map(transition -> { + ProductRequirementStatusTransitionRespVO vo = new ProductRequirementStatusTransitionRespVO(); + 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 + @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 + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId", + permission = PRODUCT_UPDATE_PERMISSION) + public Long createRequirementModule(ProductRequirementModuleSaveReqVO reqVO) { + // 校验模块名称在同一产品下是否唯一 + validateModuleNameUnique(reqVO.getProductId(), null, reqVO.getModuleName()); + + ProductRequirementModuleDO module = new ProductRequirementModuleDO(); + module.setParentId(reqVO.getParentId() != null ? reqVO.getParentId() : 0L); + module.setProductId(reqVO.getProductId()); + module.setModuleName(reqVO.getModuleName().trim()); + module.setRemark(normalizeNullableText(reqVO.getRemark())); + module.setIcon(normalizeNullableText(reqVO.getIcon())); + module.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0); + moduleMapper.insert(module); + return module.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId", + permission = PRODUCT_UPDATE_PERMISSION) + public void updateRequirementModule(ProductRequirementModuleSaveReqVO reqVO) { + if (reqVO.getId() == null) { + throw invalidParamException("模块编号不能为空"); + } + ProductRequirementModuleDO module = validateModuleExists(reqVO.getId()); + // 校验模块名称唯一性 + validateModuleNameUnique(reqVO.getProductId(), reqVO.getId(), reqVO.getModuleName()); + + module.setModuleName(reqVO.getModuleName().trim()); + module.setRemark(normalizeNullableText(reqVO.getRemark())); + module.setIcon(normalizeNullableText(reqVO.getIcon())); + module.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0); + moduleMapper.updateById(module); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", + permission = PRODUCT_UPDATE_PERMISSION) + public void deleteRequirementModule(Long moduleId, Long productId) { + ProductRequirementModuleDO module = validateModuleExists(moduleId); + + // 查询模块下的所有需求 + List requirements = requirementMapper.selectListByModuleId(moduleId); + // 校验是否存在非终态需求 + for (ProductRequirementDO requirement : requirements) { + if (!TERMINAL_STATUSES.contains(requirement.getStatusCode())) { + throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS); + } + } + + // 级联软删除模块下的所有需求 + for (ProductRequirementDO requirement : requirements) { + requirementMapper.deleteById(requirement.getId()); + } + + // 删除模块 + moduleMapper.deleteById(moduleId); + } + + @Override + @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", + permission = PRODUCT_QUERY_PERMISSION) + public List getRequirementModuleTree(Long productId) { + List modules = moduleMapper.selectListByProductId(productId); + return buildModuleTree(modules, 0L); + } + + // ========== 私有辅助方法 ========== + + /** + * 构建需求响应VO(不含子需求) + */ + private ProductRequirementRespVO buildRequirementRespVO(ProductRequirementDO requirement) { + ProductRequirementRespVO respVO = BeanUtils.toBean(requirement, ProductRequirementRespVO.class); + // 查询状态名称 + ObjectStatusModelDO statusModel = statusModelMapper + .selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, 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; + } + + /** + * 构建需求响应VO(含子需求) + */ + private ProductRequirementRespVO buildRequirementRespVOWithChildren(ProductRequirementDO requirement) { + ProductRequirementRespVO respVO = buildRequirementRespVO(requirement); + // 查询子需求 + List children = requirementMapper.selectListByParentId(requirement.getId()); + if (!children.isEmpty()) { + respVO.setChildren(children.stream() + .map(this::buildRequirementRespVO) + .collect(Collectors.toList())); + } + return respVO; + } + + /** + * 构建模块树 + */ + private List buildModuleTree(List modules, Long parentId) { + return modules.stream() + .filter(m -> Objects.equals(m.getParentId(), parentId)) + .map(m -> { + ProductRequirementModuleRespVO vo = BeanUtils.toBean(m, ProductRequirementModuleRespVO.class); + vo.setChildren(buildModuleTree(modules, m.getId())); + return vo; + }) + .collect(Collectors.toList()); + } + + /** + * 校验需求是否存在 + */ + @VisibleForTesting + ProductRequirementDO validateRequirementExists(Long id) { + if (id == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS); + } + ProductRequirementDO requirement = requirementMapper.selectById(id); + if (requirement == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS); + } + return requirement; + } + + /** + * 校验需求是否允许编辑(终态不允许编辑) + */ + @VisibleForTesting + void validateRequirementEditable(ProductRequirementDO requirement) { + if (TERMINAL_STATUSES.contains(requirement.getStatusCode())) { + throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT); + } + } + + /** + * 校验父需求是否允许拆分 + */ + @VisibleForTesting + void validateParentAllowSplit(ProductRequirementDO parentRequirement) { + String status = parentRequirement.getStatusCode(); + if (!STATUS_PENDING_DISPATCH.equals(status) && !STATUS_IMPLEMENTING.equals(status)) { + throw exception(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT); + } + } + + /** + * 校验状态流转是否合法 + */ + @VisibleForTesting + ObjectStatusTransitionDO validateRequirementTransition(String fromStatusCode, String actionCode) { + ObjectStatusTransitionDO transition = statusTransitionMapper + .selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode); + if (transition == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode); + } + return transition; + } + + /** + * 校验状态流转是否需要填写原因 + */ + @VisibleForTesting + void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) { + if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) { + throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode()); + } + } + + /** + * 校验模块是否存在 + */ + @VisibleForTesting + ProductRequirementModuleDO validateModuleExists(Long id) { + if (id == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS); + } + ProductRequirementModuleDO module = moduleMapper.selectById(id); + if (module == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS); + } + return module; + } + + /** + * 校验模块名称在同一产品下是否唯一 + */ + @VisibleForTesting + void validateModuleNameUnique(Long productId, Long moduleId, String moduleName) { + if (!StringUtils.hasText(moduleName)) { + return; + } + ProductRequirementModuleDO existModule = moduleMapper + .selectByProductIdAndModuleName(productId, moduleName.trim()); + if (existModule == null) { + return; + } + if (moduleId == null || !existModule.getId().equals(moduleId)) { + throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NAME_DUPLICATE, moduleName.trim()); + } + } + + /** + * 解析模块ID:当未选择模块时,自动归属到该产品的"全部需求"模块 + * "全部需求"模块的标志性特征是 parentId = 0L + */ + @VisibleForTesting + Long resolveModuleId(Long moduleId, Long productId) { + if (moduleId != null) { + return moduleId; + } + // 查询该产品的"全部需求"模块(parentId = 0L 的根模块) + ProductRequirementModuleDO defaultModule = moduleMapper + .selectByProductIdAndParentId(productId, 0L); + if (defaultModule == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS); + } + return defaultModule.getId(); + } + + /** + * 校验模块是否属于当前产品 + */ + @VisibleForTesting + void validateModuleBelongsToProduct(Long moduleId, Long productId) { + if (moduleId == null) { + return; // 全部需求模块不需要校验 + } + ProductRequirementModuleDO module = moduleMapper.selectById(moduleId); + if (module == null) { + throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS); + } + if (!Objects.equals(module.getProductId(), productId)) { + throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_BELONG_TO_PRODUCT); + } + } + + /** + * 写入需求状态变更日志 + */ + private void writeRequirementStatusLog(ProductRequirementDO requirement, String actionType, + String fromStatus, String toStatus, String reason) { + ProductRequirementStatusLogDO statusLog = new ProductRequirementStatusLogDO(); + statusLog.setRequirementId(requirement.getId()); + statusLog.setActionType(actionType); + statusLog.setFromStatus(fromStatus); + statusLog.setToStatus(toStatus); + statusLog.setReason(defaultText(reason)); + statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + statusLog.setRequirementTitleSnapshot(requirement.getTitle()); + statusLogMapper.insert(statusLog); + } + + /** + * 写入业务审计日志 + */ + private void writeBizAuditLog(ProductRequirementDO requirement, String actionType, String fromStatus, + String toStatus, String fieldChanges, String reason) { + BizAuditLogDO auditLog = new BizAuditLogDO(); + auditLog.setBizType(BIZ_TYPE_REQUIREMENT); + auditLog.setBizId(requirement.getId()); + auditLog.setActionType(actionType); + auditLog.setFromStatus(fromStatus); + auditLog.setToStatus(toStatus); + auditLog.setFieldChanges(fieldChanges); + auditLog.setReason(reason); + auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + bizAuditLogMapper.insert(auditLog); + } + + /** + * 处理可能为空的文本,去除首尾空格后若为空则返回null + */ + private String normalizeNullableText(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } + + /** + * 处理可能为空的文本,若为空则返回空字符串 + */ + private String defaultText(String value) { + return StringUtils.hasText(value) ? value : ""; + } + +}