Compare commits
7 Commits
ae90dcec24
...
2026-04
| Author | SHA1 | Date | |
|---|---|---|---|
| 36752d1d15 | |||
| 73360d70ce | |||
| 7913c210cd | |||
| 06d29210ba | |||
| b4e1aae062 | |||
| 9ad7e063c0 | |||
| 846348e1aa |
@@ -34,4 +34,23 @@ 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, "模块下存在非终态需求,不可删除");
|
||||
ErrorCode REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_002_013, "存在子需求,请先删除子需求");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待确认、待评审、待分流状态的需求才能删除");
|
||||
ErrorCode REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_002_015, "存在子模块,请先删除子模块");
|
||||
ErrorCode REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_002_016, "模块下存在需求,请先删除需求");
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
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.*;
|
||||
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<Long> createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) {
|
||||
return success(requirementService.createRequirement(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新产品需求")
|
||||
public CommonResult<Boolean> 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<ProductRequirementRespVO> getRequirement(@RequestParam("id") Long id,
|
||||
@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getRequirement(id, productId));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取需求分页列表")
|
||||
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementPage(@Valid ProductRequirementPageReqVO pageReqVO) {
|
||||
return success(requirementService.getRequirementPage(pageReqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
@Operation(summary = "获取需求树形列表(分页)")
|
||||
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementTree(@Valid ProductRequirementPageReqVO pageReqVO) {
|
||||
return success(requirementService.getRequirementTree(pageReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/change-status")
|
||||
@Operation(summary = "变更需求状态")
|
||||
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) {
|
||||
requirementService.changeRequirementStatus(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除产品需求")
|
||||
public CommonResult<Boolean> deleteRequirement(@Valid @RequestBody ProductRequirementDeleteReqVO reqVO) {
|
||||
requirementService.deleteRequirement(reqVO.getId(), reqVO.getProductId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/split")
|
||||
@Operation(summary = "拆分产品需求")
|
||||
public CommonResult<Long> splitRequirement(@Valid @RequestBody ProductRequirementSplitReqVO reqVO) {
|
||||
System.out.println("-----------------------");
|
||||
System.out.println(reqVO);
|
||||
return success(requirementService.splitRequirement(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/close")
|
||||
@Operation(summary = "关闭产品需求")
|
||||
public CommonResult<Boolean> 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<List<ProductRequirementStatusTransitionRespVO>> 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<ProductRequirementLifecycleRespVO> getRequirementLifecycle(
|
||||
@RequestParam("requirementId") Long requirementId,
|
||||
@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getRequirementLifecycle(requirementId, productId));
|
||||
}
|
||||
|
||||
// ========== 模块管理 ==========
|
||||
|
||||
@PostMapping("/module/create")
|
||||
@Operation(summary = "创建需求模块")
|
||||
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
|
||||
return success(requirementService.createRequirementModule(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/module/update")
|
||||
@Operation(summary = "更新需求模块")
|
||||
public CommonResult<Boolean> updateRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
|
||||
requirementService.updateRequirementModule(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/module/delete")
|
||||
@Operation(summary = "删除需求模块")
|
||||
public CommonResult<Boolean> deleteRequirementModule(@Valid @RequestBody ProductRequirementModuleDeleteReqVO reqVO) {
|
||||
requirementService.deleteRequirementModule(reqVO.getId(), reqVO.getProductId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/module/tree")
|
||||
@Operation(summary = "获取需求模块树")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<List<ProductRequirementModuleRespVO>> getRequirementModuleTree(@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getRequirementModuleTree(productId));
|
||||
}
|
||||
|
||||
@GetMapping("/status/dict")
|
||||
@Operation(summary = "获取需求所有状态字典")
|
||||
public CommonResult<List<ProductRequirementStatusDictRespVO>> getRequirementStatusDict() {
|
||||
return success(requirementService.getRequirementStatusDict());
|
||||
}
|
||||
|
||||
@GetMapping("/status/dict/terminal")
|
||||
@Operation(summary = "获取需求终止态状态字典")
|
||||
public CommonResult<List<ProductRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
|
||||
return success(requirementService.getRequirementTerminalStatusDict());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求删除 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求删除 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementDeleteReqVO {
|
||||
@Schema(description = "需求ID(编辑时传入)", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
}
|
||||
@@ -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<ProductRequirementStatusTransitionRespVO> availableActions;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求模块删除 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求模块删除 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementModuleDeleteReqVO {
|
||||
@Schema(description = "模块ID(编辑时传入)", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
}
|
||||
@@ -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 ProductRequirementModuleReqVO {
|
||||
|
||||
@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;
|
||||
|
||||
}
|
||||
@@ -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<ProductRequirementModuleRespVO> children;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求分页 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class 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列表(包含子模块,用于IN查询)", example = "[1024, 1025]")
|
||||
private List<Long> moduleIds;
|
||||
|
||||
@Schema(description = "父需求ID(查询子需求时使用)", example = "1024")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "标题关键词", example = "模块")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求分类字典值", example = "function")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(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;
|
||||
|
||||
}
|
||||
@@ -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 = "<p>详细描述需求内容</p>")
|
||||
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<ProductRequirementRespVO> children;
|
||||
|
||||
@Schema(description = "是否为终态(已拒绝、已取消、已关闭)", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
}
|
||||
@@ -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 = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", 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;
|
||||
|
||||
}
|
||||
@@ -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 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 = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
@NotBlank(message = "需求标题不能为空")
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", 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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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 = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@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;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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 ProductRequirementStatusDictRespVO {
|
||||
|
||||
@Schema(description = "状态编码", example = "pending_confirm")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "状态名称", example = "待确认")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "排序值", example = "1")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "是否初始状态", example = "true")
|
||||
private Boolean initialFlag;
|
||||
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminalFlag;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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 = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", 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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
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<ProductRequirementDO> {
|
||||
|
||||
/**
|
||||
* 分页查询需求列表
|
||||
*/
|
||||
default PageResult<ProductRequirementDO> selectPage(ProductRequirementPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ProductRequirementDO> queryWrapper = buildQueryWrapper(reqVO);
|
||||
return selectPage(reqVO, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有符合条件的需求列表(不分页,用于树形查询)
|
||||
*/
|
||||
default List<ProductRequirementDO> selectList(ProductRequirementPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ProductRequirementDO> queryWrapper = buildQueryWrapper(reqVO);
|
||||
return selectList(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*/
|
||||
private LambdaQueryWrapperX<ProductRequirementDO> buildQueryWrapper(ProductRequirementPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ProductRequirementDO> 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列表IN查询(优先使用moduleIds,用于支持子模块查询)
|
||||
.inIfPresent(ProductRequirementDO::getModuleId, reqVO.getModuleIds())
|
||||
// 模块ID精确匹配(当moduleIds为空时使用)
|
||||
.eqIfPresent(ProductRequirementDO::getModuleId, reqVO.getModuleId())
|
||||
// 父需求ID精确匹配(查询子需求时使用)
|
||||
.eqIfPresent(ProductRequirementDO::getParentId, reqVO.getParentId())
|
||||
// 产品ID精确匹配
|
||||
.eq(ProductRequirementDO::getProductId, reqVO.getProductId())
|
||||
// 按排序值升序,再按创建时间降序
|
||||
.orderByAsc(ProductRequirementDO::getSort)
|
||||
.orderByDesc(ProductRequirementDO::getCreateTime);
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID和模块ID查询需求列表
|
||||
*/
|
||||
default List<ProductRequirementDO> selectListByProductIdAndModuleId(Long productId, Long moduleId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getProductId, productId)
|
||||
.eq(ProductRequirementDO::getModuleId, moduleId)
|
||||
.orderByAsc(ProductRequirementDO::getSort)
|
||||
.orderByDesc(ProductRequirementDO::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据父需求ID查询子需求列表
|
||||
*/
|
||||
default List<ProductRequirementDO> selectListByParentId(Long parentId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getParentId, parentId)
|
||||
.orderByAsc(ProductRequirementDO::getSort)
|
||||
.orderByDesc(ProductRequirementDO::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID查询所有需求(用于模块删除时级联处理)
|
||||
*/
|
||||
default List<ProductRequirementDO> selectListByProductId(Long productId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getProductId, productId)
|
||||
.orderByAsc(ProductRequirementDO::getSort));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模块ID查询需求列表
|
||||
*/
|
||||
default List<ProductRequirementDO> selectListByModuleId(Long moduleId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getModuleId, moduleId)
|
||||
.orderByAsc(ProductRequirementDO::getSort));
|
||||
}
|
||||
|
||||
/**
|
||||
* 带并发控制的状态更新(id + fromStatus 条件更新)
|
||||
*/
|
||||
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
|
||||
return updateStatusByIdAndStatusWithProject(id, fromStatus, toStatus, lastStatusReason, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带并发控制的状态更新(支持同时更新实现项目ID)
|
||||
*/
|
||||
default int updateStatusByIdAndStatusWithProject(Long id, String fromStatus, String toStatus, String lastStatusReason, Long implementProjectId) {
|
||||
ProductRequirementDO update = new ProductRequirementDO();
|
||||
update.setStatusCode(toStatus);
|
||||
update.setLastStatusReason(lastStatusReason);
|
||||
if (implementProjectId != null) {
|
||||
update.setImplementProjectId(implementProjectId);
|
||||
}
|
||||
return update(update, new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getId, id)
|
||||
.eq(ProductRequirementDO::getStatusCode, fromStatus));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID和状态删除(带并发控制)
|
||||
*/
|
||||
default int deleteByIdAndStatus(Long id, String statusCode) {
|
||||
return delete(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getId, id)
|
||||
.eq(ProductRequirementDO::getStatusCode, statusCode));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ProductRequirementModuleDO> {
|
||||
|
||||
/**
|
||||
* 根据产品ID查询模块列表(用于构建模块树)
|
||||
*/
|
||||
default List<ProductRequirementModuleDO> selectListByProductId(Long productId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementModuleDO>()
|
||||
.eq(ProductRequirementModuleDO::getProductId, productId)
|
||||
.orderByAsc(ProductRequirementModuleDO::getSort)
|
||||
.orderByAsc(ProductRequirementModuleDO::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据父模块ID查询子模块列表
|
||||
*/
|
||||
default List<ProductRequirementModuleDO> selectListByParentId(Long parentId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementModuleDO>()
|
||||
.eq(ProductRequirementModuleDO::getParentId, parentId)
|
||||
.orderByAsc(ProductRequirementModuleDO::getSort));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID和模块名称查询模块(用于校验名称唯一性)
|
||||
*/
|
||||
default ProductRequirementModuleDO selectByProductIdAndModuleName(Long productId, String moduleName) {
|
||||
return selectOne(new LambdaQueryWrapperX<ProductRequirementModuleDO>()
|
||||
.eq(ProductRequirementModuleDO::getProductId, productId)
|
||||
.eq(ProductRequirementModuleDO::getModuleName, moduleName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID和父模块ID查询模块(用于查找产品的"全部需求"根模块)
|
||||
*/
|
||||
default ProductRequirementModuleDO selectByProductIdAndParentId(Long productId, Long parentId) {
|
||||
return selectOne(new LambdaQueryWrapperX<ProductRequirementModuleDO>()
|
||||
.eq(ProductRequirementModuleDO::getProductId, productId)
|
||||
.eq(ProductRequirementModuleDO::getParentId, parentId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ProductRequirementStatusLogDO> {
|
||||
|
||||
/**
|
||||
* 根据需求ID查询状态变更日志列表
|
||||
*/
|
||||
default List<ProductRequirementStatusLogDO> selectListByRequirementId(Long requirementId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementStatusLogDO>()
|
||||
.eq(ProductRequirementStatusLogDO::getRequirementId, requirementId)
|
||||
.orderByDesc(ProductRequirementStatusLogDO::getCreateTime));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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, Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求分页列表
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获取需求树形列表(分页)
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页结果(只按父需求分页,子需求不计入分页)
|
||||
*/
|
||||
PageResult<ProductRequirementRespVO> getRequirementTree(ProductRequirementPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 变更需求状态
|
||||
*
|
||||
* @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<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求生命周期信息(当前状态 + 可执行动作)
|
||||
*
|
||||
* @param requirementId 需求编号
|
||||
* @param productId 产品编号
|
||||
* @return 生命周期信息
|
||||
*/
|
||||
ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId);
|
||||
|
||||
// ========== 模块管理 ==========
|
||||
|
||||
/**
|
||||
* 创建需求模块
|
||||
*
|
||||
* @param reqVO 模块保存请求
|
||||
* @return 模块编号
|
||||
*/
|
||||
Long createRequirementModule(ProductRequirementModuleReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 更新需求模块
|
||||
*
|
||||
* @param reqVO 模块保存请求
|
||||
*/
|
||||
void updateRequirementModule(ProductRequirementModuleReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除需求模块(级联删除模块下需求)
|
||||
*
|
||||
* @param moduleId 模块编号
|
||||
* @param productId 产品编号
|
||||
*/
|
||||
void deleteRequirementModule(Long moduleId, Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求模块树
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 模块树
|
||||
*/
|
||||
List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求所有状态字典列表
|
||||
*
|
||||
* @return 状态字典列表
|
||||
*/
|
||||
List<ProductRequirementStatusDictRespVO> getRequirementStatusDict();
|
||||
|
||||
/**
|
||||
* 获取需求终止态状态字典列表
|
||||
*
|
||||
* @return 终止态状态字典列表
|
||||
*/
|
||||
List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,914 @@
|
||||
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.*;
|
||||
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<String> TERMINAL_STATUSES = List.of(STATUS_CLOSED, STATUS_REJECTED, STATUS_CANCELLED);
|
||||
// 子需求允许大需求关闭的状态集合
|
||||
private static final List<String> CHILD_ALLOW_CLOSE_STATUSES = List.of(STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED);
|
||||
// 允许删除的状态集合(实施中之前的状态)
|
||||
private static final List<String> ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH);
|
||||
|
||||
// 权限常量
|
||||
private static final String PRODUCT_CREATE_PERMISSION = "project:product:create";
|
||||
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_DELETE_PERMISSION = "project:product:delete";
|
||||
private static final String PRODUCT_SPLIT_PERMISSION = "project:product:split";
|
||||
|
||||
// 审计动作常量
|
||||
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_CREATE_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, Long productId) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(id);
|
||||
return buildRequirementRespVO(requirement);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#pageReqVO.productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO) {
|
||||
// 处理模块ID条件:支持递归查询子模块
|
||||
if (pageReqVO.getModuleId() != null) {
|
||||
if (isAllRequirementsModule(pageReqVO.getModuleId())) {
|
||||
// "全部需求"模块,忽略模块ID条件,查询该产品下所有需求
|
||||
pageReqVO.setModuleId(null);
|
||||
} else {
|
||||
// 非"全部需求"模块,获取该模块及其所有子模块的ID列表
|
||||
List<Long> moduleIds = getAllModuleIdsWithChildren(pageReqVO.getModuleId(), pageReqVO.getProductId());
|
||||
pageReqVO.setModuleIds(moduleIds);
|
||||
pageReqVO.setModuleId(null);
|
||||
}
|
||||
}
|
||||
PageResult<ProductRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO);
|
||||
List<ProductRequirementRespVO> list = pageResult.getList().stream()
|
||||
.map(this::buildRequirementRespVO)
|
||||
.collect(Collectors.toList());
|
||||
return new PageResult<>(list, pageResult.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#pageReqVO.productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public PageResult<ProductRequirementRespVO> getRequirementTree(ProductRequirementPageReqVO pageReqVO) {
|
||||
Long moduleId = pageReqVO.getModuleId();
|
||||
Long productId = pageReqVO.getProductId();
|
||||
|
||||
// 处理模块过滤条件:仅当选中具体模块时,递归加载子模块ID进行过滤
|
||||
if (moduleId != null) {
|
||||
pageReqVO.setModuleIds(getAllModuleIdsWithChildren(moduleId, productId));
|
||||
pageReqVO.setModuleId(null);
|
||||
}
|
||||
// 清空parentId,查询所有符合条件的需求(不限层级)
|
||||
pageReqVO.setParentId(null);
|
||||
|
||||
// 第一步:查询所有符合搜索条件的需求
|
||||
List<ProductRequirementDO> matchedRequirements = requirementMapper.selectList(pageReqVO);
|
||||
if (matchedRequirements.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
|
||||
// 第二步:找出所有匹配需求的根节点ID,同时收集路径上的所有节点ID
|
||||
Set<Long> rootIds = new HashSet<>();
|
||||
Set<Long> pathNodeIds = new HashSet<>();
|
||||
Map<Long, ProductRequirementDO> requirementCache = new HashMap<>();
|
||||
for (ProductRequirementDO req : matchedRequirements) {
|
||||
requirementCache.put(req.getId(), req);
|
||||
pathNodeIds.add(req.getId());
|
||||
Long rootId = findRootRequirementIdAndCollectPath(req, requirementCache, pathNodeIds);
|
||||
rootIds.add(rootId);
|
||||
}
|
||||
|
||||
// 第三步:查询根需求详情并按创建时间倒排
|
||||
List<ProductRequirementDO> rootRequirements = requirementMapper.selectBatchIds(rootIds);
|
||||
rootRequirements.sort((a, b) -> b.getCreateTime().compareTo(a.getCreateTime()));
|
||||
|
||||
// 第四步:对根节点列表进行内存分页
|
||||
int pageNo = pageReqVO.getPageNo() != null ? pageReqVO.getPageNo() : 1;
|
||||
int pageSize = pageReqVO.getPageSize() != null ? pageReqVO.getPageSize() : 10;
|
||||
int total = rootRequirements.size();
|
||||
int fromIndex = (pageNo - 1) * pageSize;
|
||||
int toIndex = Math.min(fromIndex + pageSize, total);
|
||||
|
||||
if (fromIndex >= total) {
|
||||
return new PageResult<>(Collections.emptyList(), (long) total);
|
||||
}
|
||||
|
||||
List<ProductRequirementDO> pagedRootRequirements = rootRequirements.subList(fromIndex, toIndex);
|
||||
|
||||
// 第五步:构建树形结构(只包含路径上的节点)
|
||||
List<ProductRequirementRespVO> list = pagedRootRequirements.stream()
|
||||
.map(req -> buildRequirementRespVOWithPathChildren(req, pathNodeIds))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new PageResult<>(list, (long) total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上追溯需求的根节点ID,同时收集路径上的所有节点ID
|
||||
* @param requirement 起始需求
|
||||
* @param cache 需求缓存(避免重复查询)
|
||||
* @param pathNodeIds 路径节点ID集合(输出参数)
|
||||
* @return 根节点ID(parentId = 0L 的需求ID)
|
||||
*/
|
||||
private Long findRootRequirementIdAndCollectPath(ProductRequirementDO requirement,
|
||||
Map<Long, ProductRequirementDO> cache,
|
||||
Set<Long> pathNodeIds) {
|
||||
if (requirement.getParentId() == null || requirement.getParentId() == 0L) {
|
||||
return requirement.getId();
|
||||
}
|
||||
// 从缓存中查找父需求,如果没有则查询数据库
|
||||
ProductRequirementDO parent = cache.get(requirement.getParentId());
|
||||
if (parent == null) {
|
||||
parent = requirementMapper.selectById(requirement.getParentId());
|
||||
if (parent != null) {
|
||||
cache.put(parent.getId(), parent);
|
||||
}
|
||||
}
|
||||
if (parent == null) {
|
||||
// 父需求不存在(数据异常),返回当前需求作为根
|
||||
return requirement.getId();
|
||||
}
|
||||
// 收集路径上的节点ID
|
||||
pathNodeIds.add(parent.getId());
|
||||
return findRootRequirementIdAndCollectPath(parent, cache, pathNodeIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建需求响应VO(只包含路径上的子需求)
|
||||
* @param requirement 需求
|
||||
* @param pathNodeIds 路径节点ID集合
|
||||
* @return 需求响应VO
|
||||
*/
|
||||
private ProductRequirementRespVO buildRequirementRespVOWithPathChildren(ProductRequirementDO requirement,
|
||||
Set<Long> pathNodeIds) {
|
||||
ProductRequirementRespVO respVO = buildRequirementRespVO(requirement);
|
||||
// 查询子需求
|
||||
List<ProductRequirementDO> allChildren = requirementMapper.selectListByParentId(requirement.getId());
|
||||
// 只保留路径上的子需求
|
||||
List<ProductRequirementDO> pathChildren = allChildren.stream()
|
||||
.filter(child -> pathNodeIds.contains(child.getId()))
|
||||
.toList();
|
||||
if (!pathChildren.isEmpty()) {
|
||||
respVO.setChildren(pathChildren.stream()
|
||||
.map(child -> buildRequirementRespVOWithPathChildren(child, pathNodeIds))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定模块是否为"全部需求"模块(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取模块及其所有子模块的ID列表
|
||||
* @param moduleId 起始模块ID
|
||||
* @param productId 产品ID
|
||||
* @return 包含自身及所有子模块的ID列表
|
||||
*/
|
||||
@VisibleForTesting
|
||||
List<Long> getAllModuleIdsWithChildren(Long moduleId, Long productId) {
|
||||
List<Long> moduleIds = new ArrayList<>();
|
||||
moduleIds.add(moduleId);
|
||||
// 查询该产品下所有模块
|
||||
List<ProductRequirementModuleDO> allModules = moduleMapper.selectListByProductId(productId);
|
||||
// 递归查找子模块
|
||||
collectChildModuleIds(moduleId, allModules, moduleIds);
|
||||
return moduleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集子模块ID
|
||||
*/
|
||||
private void collectChildModuleIds(Long parentId, List<ProductRequirementModuleDO> allModules, List<Long> result) {
|
||||
for (ProductRequirementModuleDO module : allModules) {
|
||||
if (Objects.equals(module.getParentId(), parentId)) {
|
||||
result.add(module.getId());
|
||||
collectChildModuleIds(module.getId(), allModules, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取需求及其所有子需求(包含子子需求)
|
||||
* @param requirementId 起始需求ID
|
||||
* @return 包含自身及所有后代需求的列表
|
||||
*/
|
||||
@VisibleForTesting
|
||||
List<ProductRequirementDO> getAllRequirementsWithChildren(Long requirementId) {
|
||||
List<ProductRequirementDO> allRequirements = new ArrayList<>();
|
||||
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
||||
allRequirements.add(requirement);
|
||||
collectChildRequirements(requirementId, allRequirements);
|
||||
return allRequirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集子需求
|
||||
*/
|
||||
private void collectChildRequirements(Long parentId, List<ProductRequirementDO> result) {
|
||||
List<ProductRequirementDO> children = requirementMapper.selectListByParentId(parentId);
|
||||
for (ProductRequirementDO child : children) {
|
||||
result.add(child);
|
||||
collectChildRequirements(child.getId(), result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_STATUS_PERMISSION)
|
||||
public void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
||||
String actionCode = reqVO.getActionCode().trim();
|
||||
Long implementProjectId = reqVO.getImplementProjectId();
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
|
||||
// 校验状态流转是否合法
|
||||
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
||||
String reason = normalizeNullableText(reqVO.getReason());
|
||||
// 校验是否需要填写原因
|
||||
validateTransitionReason(transition, reason);
|
||||
//下一状态
|
||||
String toStatus = transition.getToStatusCode();
|
||||
|
||||
// accept和close动作时,校验所有子需求(包括子子需求)是否处于允许状态
|
||||
if ("accept".equals(actionCode) || "close".equals(actionCode)) {
|
||||
validateAllChildrenAllowCloseOrAccept(reqVO.getId());
|
||||
}
|
||||
// close动作时,递归关闭所有已验收的子需求(包括子子需求)
|
||||
if ("close".equals(actionCode)) {
|
||||
closeAllAcceptedChildren(reqVO.getId(), reason);
|
||||
}
|
||||
// 带并发控制的状态更新(支持同时更新实现项目ID)
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatusWithProject(requirement.getId(), fromStatus, toStatus, reason, implementProjectId);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验需求的所有子需求(包括子子需求)是否处于允许关闭或验收的状态
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
|
||||
List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
||||
// 排除自身,只校验子需求
|
||||
for (ProductRequirementDO req : allChildren) {
|
||||
if (!Objects.equals(req.getId(), requirementId)) {
|
||||
if (!CHILD_ALLOW_CLOSE_STATUSES.contains(req.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_DELETE_PERMISSION)
|
||||
public void deleteRequirement(Long id, Long productId) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(id);
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
|
||||
// 校验是否存在子需求
|
||||
List<ProductRequirementDO> children = requirementMapper.selectListByParentId(id);
|
||||
if (!children.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_HAS_CHILDREN);
|
||||
}
|
||||
|
||||
// 校验状态是否允许删除(只有待确认、待评审、待分流状态才能删除)
|
||||
if (!ALLOW_DELETE_STATUSES.contains(fromStatus)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_DELETE);
|
||||
}
|
||||
|
||||
// 带并发控制的删除(以当前状态作为条件)
|
||||
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_SPLIT_PERMISSION)
|
||||
public Long splitRequirement(ProductRequirementSplitReqVO reqVO) {
|
||||
// 校验父需求是否存在
|
||||
ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId());
|
||||
// 校验父需求状态是否允许拆分(只能是待分流或实施中)
|
||||
validateParentAllowSplit(parentRequirement);
|
||||
|
||||
// 创建子需求
|
||||
ProductRequirementDO childRequirement = new ProductRequirementDO();
|
||||
childRequirement.setParentId(reqVO.getParentId());
|
||||
childRequirement.setProductId(parentRequirement.getProductId()); // 子需求继承父需求的产品
|
||||
childRequirement.setModuleId(parentRequirement.getModuleId()); // 子需求继承父需求的模块
|
||||
childRequirement.setReviewRequired(reqVO.getReviewRequired()); // 子需求默认不需要评审
|
||||
childRequirement.setTitle(reqVO.getTitle().trim());
|
||||
childRequirement.setDescription(normalizeNullableText(reqVO.getDescription()));
|
||||
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;
|
||||
childRequirement.setStatusCode(initialStatus);
|
||||
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, initialStatus, null, null);
|
||||
return childRequirement.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_STATUS_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);
|
||||
}
|
||||
|
||||
// 校验所有子需求(包括子子需求)是否允许关闭
|
||||
validateAllChildrenAllowCloseOrAccept(reqVO.getId());
|
||||
|
||||
// 递归关闭所有已验收的子需求(包括子子需求)
|
||||
closeAllAcceptedChildren(reqVO.getId(), 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归关闭所有已验收的子需求(包括子子需求)
|
||||
*/
|
||||
private void closeAllAcceptedChildren(Long parentId, String reason) {
|
||||
List<ProductRequirementDO> children = requirementMapper.selectListByParentId(parentId);
|
||||
for (ProductRequirementDO child : children) {
|
||||
// 递归处理子需求的子需求
|
||||
closeAllAcceptedChildren(child.getId(), reason);
|
||||
// 如果子需求已验收,则关闭
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
||||
String currentStatus = requirement.getStatusCode();
|
||||
|
||||
// 查询当前状态允许的所有流转
|
||||
List<ObjectStatusTransitionDO> 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_CREATE_PERMISSION)
|
||||
public Long createRequirementModule(ProductRequirementModuleReqVO 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(ProductRequirementModuleReqVO 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_DELETE_PERMISSION)
|
||||
public void deleteRequirementModule(Long moduleId, Long productId) {
|
||||
validateModuleExists(moduleId);
|
||||
|
||||
// 校验是否存在子模块
|
||||
List<ProductRequirementModuleDO> childModules = moduleMapper.selectListByParentId(moduleId);
|
||||
if (!childModules.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_CHILDREN);
|
||||
}
|
||||
|
||||
// 校验模块下是否存在需求
|
||||
List<ProductRequirementDO> requirements = requirementMapper.selectListByModuleId(moduleId);
|
||||
if (!requirements.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_REQUIREMENTS);
|
||||
}
|
||||
|
||||
// 删除模块
|
||||
moduleMapper.deleteById(moduleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId) {
|
||||
List<ProductRequirementModuleDO> modules = moduleMapper.selectListByProductId(productId);
|
||||
return buildModuleTree(modules, 0L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProductRequirementStatusDictRespVO> getRequirementStatusDict() {
|
||||
List<ObjectStatusModelDO> statusModels = statusModelMapper.selectListByObjectType(REQUIREMENT_OBJECT_TYPE);
|
||||
return statusModels.stream()
|
||||
.map(this::buildStatusDictRespVO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict() {
|
||||
List<ObjectStatusModelDO> statusModels = statusModelMapper.selectListByObjectType(REQUIREMENT_OBJECT_TYPE);
|
||||
return statusModels.stream()
|
||||
.filter(ObjectStatusModelDO::getTerminalFlag)
|
||||
.map(this::buildStatusDictRespVO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建状态字典响应VO
|
||||
*/
|
||||
private ProductRequirementStatusDictRespVO buildStatusDictRespVO(ObjectStatusModelDO statusModel) {
|
||||
ProductRequirementStatusDictRespVO respVO = new ProductRequirementStatusDictRespVO();
|
||||
respVO.setStatusCode(statusModel.getStatusCode());
|
||||
respVO.setStatusName(statusModel.getStatusName());
|
||||
respVO.setSort(statusModel.getSort());
|
||||
respVO.setInitialFlag(statusModel.getInitialFlag());
|
||||
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
// ========== 私有辅助方法 ==========
|
||||
|
||||
/**
|
||||
* 构建需求响应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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建模块树
|
||||
*/
|
||||
private List<ProductRequirementModuleRespVO> buildModuleTree(List<ProductRequirementModuleDO> 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 (!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 : "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,16 +6,8 @@ import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextNavRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextProductRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRoleRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.*;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
@@ -24,9 +16,11 @@ import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransition
|
||||
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductStatusLogMapper;
|
||||
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 com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectMenuRespDTO;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectRolePermissionRespDTO;
|
||||
@@ -41,14 +35,7 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -93,6 +80,8 @@ public class ProductServiceImpl implements ProductService {
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
@Resource
|
||||
private ProductRequirementModuleMapper requirementModuleMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -110,6 +99,7 @@ public class ProductServiceImpl implements ProductService {
|
||||
productMapper.insert(product);
|
||||
|
||||
initManagerMemberRelation(product);
|
||||
initDefaultRequirementModule(product);
|
||||
writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_CREATE, null, PRODUCT_ACTIVE_STATUS,
|
||||
buildProductFieldChanges(null, product), null);
|
||||
return product.getId();
|
||||
@@ -363,6 +353,17 @@ public class ProductServiceImpl implements ProductService {
|
||||
return generatedCode;
|
||||
}
|
||||
|
||||
private void initDefaultRequirementModule(ProductDO product) {
|
||||
com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO module =
|
||||
new com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO();
|
||||
module.setParentId(0L);
|
||||
module.setProductId(product.getId());
|
||||
module.setModuleName("全部需求");
|
||||
module.setRemark("自动创建的模块");
|
||||
module.setSort(0);
|
||||
requirementModuleMapper.insert(module);
|
||||
}
|
||||
|
||||
private void initManagerMemberRelation(ProductDO product) {
|
||||
ObjectRoleRespDTO managerRole = objectPermissionApi
|
||||
.getObjectRoleByCode(PRODUCT_MANAGER_ROLE_CODE, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
|
||||
|
||||
@@ -0,0 +1,671 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
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.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 org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 产品需求 Service 单元测试
|
||||
*/
|
||||
class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private ProductRequirementServiceImpl requirementService;
|
||||
@Mock
|
||||
private ProductRequirementMapper requirementMapper;
|
||||
@Mock
|
||||
private ProductRequirementModuleMapper moduleMapper;
|
||||
@Mock
|
||||
private ProductRequirementStatusLogMapper statusLogMapper;
|
||||
@Mock
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@Mock
|
||||
private ObjectStatusTransitionMapper statusTransitionMapper;
|
||||
@Mock
|
||||
private ObjectStatusModelMapper statusModelMapper;
|
||||
|
||||
// ========== 创建需求测试 ==========
|
||||
|
||||
@Test
|
||||
void createRequirement_withoutReview_shouldCreateWithPendingDispatchStatus() {
|
||||
Long loginUserId = 1001L;
|
||||
Long defaultModuleId = 50L;
|
||||
ProductRequirementSaveReqVO reqVO = new ProductRequirementSaveReqVO();
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("测试需求");
|
||||
reqVO.setDescription("测试描述");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setReviewRequired(0); // 不需要评审
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setCurrentHandlerUserId(2002L);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
// 模拟"全部需求"模块存在(parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.createRequirement(reqVO);
|
||||
}
|
||||
|
||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).insert(captor.capture());
|
||||
ProductRequirementDO created = captor.getValue();
|
||||
assertEquals("pending_dispatch", created.getStatusCode()); // 不需要评审时初始状态为待分流
|
||||
assertEquals("manual", created.getSourceType());
|
||||
assertEquals(0L, created.getParentId());
|
||||
assertEquals("测试需求", created.getTitle());
|
||||
assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
||||
assertEquals(100L, created.getProductId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRequirement_withReview_shouldCreateWithPendingReviewStatus() {
|
||||
Long loginUserId = 1001L;
|
||||
Long defaultModuleId = 50L;
|
||||
ProductRequirementSaveReqVO reqVO = new ProductRequirementSaveReqVO();
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("需评审的需求");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setReviewRequired(1); // 需要评审
|
||||
reqVO.setPriority(2);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
// 模拟"全部需求"模块存在(parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.createRequirement(reqVO);
|
||||
}
|
||||
|
||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).insert(captor.capture());
|
||||
ProductRequirementDO created = captor.getValue();
|
||||
assertEquals("pending_review", created.getStatusCode()); // 需要评审时初始状态为待评审
|
||||
assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
||||
}
|
||||
|
||||
// ========== 更新需求测试 ==========
|
||||
|
||||
@Test
|
||||
void updateRequirement_whenTerminalStatus_shouldThrowException() {
|
||||
Long requirementId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已关闭需求",
|
||||
"closed", 0L, 0);
|
||||
ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("修改标题");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setReviewRequired(0);
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.updateRequirement(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT.getCode(), ex.getCode());
|
||||
verify(requirementMapper, never()).updateById(any(ProductRequirementDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateRequirement_whenNormalStatus_shouldUpdateSuccessfully() {
|
||||
Long requirementId = 1002L;
|
||||
Long loginUserId = 1001L;
|
||||
Long defaultModuleId = 50L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("修改后的标题");
|
||||
reqVO.setCategory("security");
|
||||
reqVO.setReviewRequired(0);
|
||||
reqVO.setPriority(2);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
// 模拟"全部需求"模块存在(未选择模块时自动归属,parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.updateRequirement(reqVO);
|
||||
}
|
||||
|
||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).updateById(captor.capture());
|
||||
assertEquals(defaultModuleId, captor.getValue().getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
// ========== 状态变更测试 ==========
|
||||
|
||||
@Test
|
||||
void changeRequirementStatus_whenActionAllowed_shouldUpdateStatusAndWriteLogs() {
|
||||
Long requirementId = 1003L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setActionCode("dispatch");
|
||||
reqVO.setReason(null);
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
"product_requirement", "pending_dispatch", "dispatch"))
|
||||
.thenReturn(createTransition("dispatch", "implementing", false));
|
||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null))
|
||||
.thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.changeRequirementStatus(reqVO);
|
||||
}
|
||||
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null);
|
||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeRequirementStatus_whenActionNotAllowed_shouldThrowException() {
|
||||
Long requirementId = 1004L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已关闭需求",
|
||||
"closed", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setActionCode("dispatch");
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
"product_requirement", "closed", "dispatch"))
|
||||
.thenReturn(null);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.changeRequirementStatus(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeRequirementStatus_whenReasonRequiredButMissing_shouldThrowException() {
|
||||
Long requirementId = 1005L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setActionCode("cancel");
|
||||
reqVO.setReason(" "); // 空原因
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
"product_requirement", "pending_dispatch", "cancel"))
|
||||
.thenReturn(createTransition("cancel", "cancelled", true));
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.changeRequirementStatus(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeRequirementStatus_whenConcurrentModified_shouldThrowException() {
|
||||
Long requirementId = 1006L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setActionCode("dispatch");
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
"product_requirement", "pending_dispatch", "dispatch"))
|
||||
.thenReturn(createTransition("dispatch", "implementing", false));
|
||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null))
|
||||
.thenReturn(0); // 并发修改,更新失败
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.changeRequirementStatus(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED.getCode(), ex.getCode());
|
||||
}
|
||||
verify(statusLogMapper, never()).insert(any(ProductRequirementStatusLogDO.class));
|
||||
verify(bizAuditLogMapper, never()).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
// ========== 删除需求测试 ==========
|
||||
|
||||
@Test
|
||||
void deleteRequirement_shouldDeleteByConditionAndWriteLogs() {
|
||||
Long requirementId = 1007L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(requirementMapper.deleteByIdAndStatus(requirementId, "pending_dispatch")).thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.deleteRequirement(requirementId, 100L);
|
||||
}
|
||||
|
||||
verify(requirementMapper, times(1)).deleteByIdAndStatus(requirementId, "pending_dispatch");
|
||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
// ========== 拆分需求测试 ==========
|
||||
|
||||
@Test
|
||||
void splitRequirement_whenParentPendingDispatch_shouldChangeParentToImplementing() {
|
||||
Long parentId = 1008L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "大需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
parent.setModuleId(50L);
|
||||
parent.setProposerId(2001L);
|
||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
||||
reqVO.setParentId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("子需求");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
when(requirementMapper.updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null))
|
||||
.thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.splitRequirement(reqVO);
|
||||
}
|
||||
|
||||
// 验证子需求被创建
|
||||
ArgumentCaptor<ProductRequirementDO> childCaptor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).insert(childCaptor.capture());
|
||||
ProductRequirementDO child = childCaptor.getValue();
|
||||
assertEquals(parentId, child.getParentId());
|
||||
assertEquals("pending_dispatch", child.getStatusCode());
|
||||
assertEquals(parent.getModuleId(), child.getModuleId());
|
||||
assertEquals(parent.getProposerId(), child.getProposerId());
|
||||
|
||||
// 验证父需求状态变为实施中
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null);
|
||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitRequirement_whenParentImplementing_shouldNotChangeParentStatus() {
|
||||
Long parentId = 1009L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "实施中的大需求",
|
||||
"implementing", 0L, 0);
|
||||
parent.setModuleId(50L);
|
||||
parent.setProposerId(2001L);
|
||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
||||
reqVO.setParentId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("子需求2");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.splitRequirement(reqVO);
|
||||
}
|
||||
|
||||
// 父需求已经是实施中,不需要再更新状态
|
||||
verify(requirementMapper, never())
|
||||
.updateStatusByIdAndStatus(parentId, "implementing", "implementing", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitRequirement_whenParentNotAllowSplit_shouldThrowException() {
|
||||
Long parentId = 1010L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "已验收的大需求",
|
||||
"accepted", 0L, 0);
|
||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
||||
reqVO.setParentId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("子需求");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setCompletionDate(LocalDateTime.now());
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.splitRequirement(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT.getCode(), ex.getCode());
|
||||
verify(requirementMapper, never()).insert(any(ProductRequirementDO.class));
|
||||
}
|
||||
|
||||
// ========== 关闭需求测试 ==========
|
||||
|
||||
@Test
|
||||
void closeRequirement_whenAccepted_shouldCloseSuccessfully() {
|
||||
Long requirementId = 1011L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已验收需求",
|
||||
"accepted", 0L, 0);
|
||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setReason("需求已完成");
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(requirementMapper.selectListByParentId(requirementId)).thenReturn(Collections.emptyList());
|
||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "accepted", "closed", "需求已完成"))
|
||||
.thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.closeRequirement(reqVO);
|
||||
}
|
||||
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(requirementId, "accepted", "closed", "需求已完成");
|
||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeRequirement_whenNotAccepted_shouldThrowException() {
|
||||
Long requirementId = 1012L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "实施中需求",
|
||||
"implementing", 0L, 0);
|
||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setReason("需求已完成");
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.closeRequirement(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_CLOSE.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeRequirement_withChildren_shouldCascadeCloseAcceptedChildren() {
|
||||
Long parentId = 1013L;
|
||||
Long childId = 1014L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "父需求",
|
||||
"accepted", 0L, 0);
|
||||
ProductRequirementDO child = createRequirement(childId, 100L, "子需求",
|
||||
"accepted", parentId, 0);
|
||||
|
||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
||||
reqVO.setId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setReason("全部验收完成");
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
when(requirementMapper.selectListByParentId(parentId)).thenReturn(List.of(child));
|
||||
when(requirementMapper.updateStatusByIdAndStatus(childId, "accepted", "closed", "全部验收完成"))
|
||||
.thenReturn(1);
|
||||
when(requirementMapper.updateStatusByIdAndStatus(parentId, "accepted", "closed", "全部验收完成"))
|
||||
.thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.closeRequirement(reqVO);
|
||||
}
|
||||
|
||||
// 子需求被关闭
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(childId, "accepted", "closed", "全部验收完成");
|
||||
// 父需求被关闭
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(parentId, "accepted", "closed", "全部验收完成");
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeRequirement_withChildNotAllowClose_shouldThrowException() {
|
||||
Long parentId = 1015L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "父需求",
|
||||
"accepted", 0L, 0);
|
||||
ProductRequirementDO child = createRequirement(1016L, 100L, "实施中子需求",
|
||||
"implementing", parentId, 0);
|
||||
|
||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
||||
reqVO.setId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setReason("全部验收完成");
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
when(requirementMapper.selectListByParentId(parentId)).thenReturn(List.of(child));
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.closeRequirement(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
// ========== 模块管理测试 ==========
|
||||
|
||||
@Test
|
||||
void createRequirementModule_whenNameDuplicate_shouldThrowException() {
|
||||
Long productId = 100L;
|
||||
ProductRequirementModuleDO existModule = new ProductRequirementModuleDO();
|
||||
existModule.setId(50L);
|
||||
existModule.setProductId(productId);
|
||||
existModule.setModuleName("核心功能");
|
||||
|
||||
ProductRequirementModuleReqVO reqVO = new ProductRequirementModuleReqVO();
|
||||
reqVO.setProductId(productId);
|
||||
reqVO.setModuleName("核心功能");
|
||||
|
||||
when(moduleMapper.selectByProductIdAndModuleName(productId, "核心功能")).thenReturn(existModule);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.createRequirementModule(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_NAME_DUPLICATE.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRequirementModule_whenHasNonTerminalRequirements_shouldThrowException() {
|
||||
Long moduleId = 50L;
|
||||
Long productId = 100L;
|
||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
||||
module.setId(moduleId);
|
||||
module.setProductId(productId);
|
||||
module.setModuleName("核心功能");
|
||||
|
||||
ProductRequirementDO nonTerminalReq = createRequirement(2001L, productId, "实施中需求",
|
||||
"implementing", 0L, 0);
|
||||
nonTerminalReq.setModuleId(moduleId);
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(module);
|
||||
when(requirementMapper.selectListByModuleId(moduleId)).thenReturn(List.of(nonTerminalReq));
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.deleteRequirementModule(moduleId, productId));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS.getCode(), ex.getCode());
|
||||
verify(moduleMapper, never()).deleteById(moduleId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRequirementModule_whenAllTerminal_shouldDeleteSuccessfully() {
|
||||
Long moduleId = 50L;
|
||||
Long productId = 100L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
||||
module.setId(moduleId);
|
||||
module.setProductId(productId);
|
||||
module.setModuleName("核心功能");
|
||||
|
||||
ProductRequirementDO closedReq = createRequirement(2001L, productId, "已关闭需求",
|
||||
"closed", 0L, 0);
|
||||
closedReq.setModuleId(moduleId);
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(module);
|
||||
when(requirementMapper.selectListByModuleId(moduleId)).thenReturn(List.of(closedReq));
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.deleteRequirementModule(moduleId, productId);
|
||||
}
|
||||
|
||||
verify(requirementMapper, times(1)).deleteById(2001L);
|
||||
verify(moduleMapper, times(1)).deleteById(moduleId);
|
||||
}
|
||||
|
||||
// ========== resolveModuleId 测试 ==========
|
||||
|
||||
@Test
|
||||
void resolveModuleId_whenModuleIdIsNull_shouldReturnDefaultModuleId() {
|
||||
Long defaultModuleId = 50L;
|
||||
Long productId = 100L;
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, productId);
|
||||
|
||||
when(moduleMapper.selectByProductIdAndParentId(productId, 0L)).thenReturn(defaultModule);
|
||||
|
||||
Long result = requirementService.resolveModuleId(null, productId);
|
||||
|
||||
assertEquals(defaultModuleId, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveModuleId_whenModuleIdProvided_shouldReturnSameId() {
|
||||
Long moduleId = 60L;
|
||||
Long productId = 100L;
|
||||
|
||||
// 传入有效moduleId时直接返回,不查询数据库
|
||||
Long result = requirementService.resolveModuleId(moduleId, productId);
|
||||
|
||||
assertEquals(moduleId, result);
|
||||
verify(moduleMapper, never()).selectByProductIdAndParentId(any(), any());
|
||||
}
|
||||
|
||||
// ========== isAllRequirementsModule 测试 ==========
|
||||
|
||||
@Test
|
||||
void isAllRequirementsModule_whenRootModule_shouldReturnTrue() {
|
||||
Long moduleId = 50L;
|
||||
ProductRequirementModuleDO rootModule = createDefaultModule(moduleId, 100L);
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(rootModule);
|
||||
|
||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
||||
|
||||
assertEquals(true, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllRequirementsModule_whenChildModule_shouldReturnFalse() {
|
||||
Long moduleId = 60L;
|
||||
ProductRequirementModuleDO childModule = new ProductRequirementModuleDO();
|
||||
childModule.setId(moduleId);
|
||||
childModule.setProductId(100L);
|
||||
childModule.setParentId(50L); // parentId != 0
|
||||
childModule.setModuleName("子模块");
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(childModule);
|
||||
|
||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
||||
|
||||
assertEquals(false, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllRequirementsModule_whenModuleNotExists_shouldReturnFalse() {
|
||||
Long moduleId = 70L;
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(null);
|
||||
|
||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
||||
|
||||
assertEquals(false, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveModuleId_whenDefaultModuleNotExists_shouldThrowException() {
|
||||
Long productId = 100L;
|
||||
|
||||
when(moduleMapper.selectByProductIdAndParentId(productId, 0L)).thenReturn(null);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.resolveModuleId(null, productId));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
// ========== 辅助方法 ==========
|
||||
|
||||
private ProductRequirementDO createRequirement(Long id, Long productId, String title,
|
||||
String statusCode, Long parentId, Integer reviewRequired) {
|
||||
ProductRequirementDO requirement = new ProductRequirementDO();
|
||||
requirement.setId(id);
|
||||
requirement.setProductId(productId);
|
||||
requirement.setTitle(title);
|
||||
requirement.setStatusCode(statusCode);
|
||||
requirement.setParentId(parentId);
|
||||
requirement.setReviewRequired(reviewRequired);
|
||||
requirement.setCategory("function");
|
||||
requirement.setSourceType("manual");
|
||||
requirement.setPriority(1);
|
||||
requirement.setProposerId(2001L);
|
||||
requirement.setSort(0);
|
||||
return requirement;
|
||||
}
|
||||
|
||||
private ProductRequirementModuleDO createDefaultModule(Long id, Long productId) {
|
||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
||||
module.setId(id);
|
||||
module.setProductId(productId);
|
||||
module.setParentId(0L);
|
||||
module.setModuleName("全部需求");
|
||||
module.setRemark("自动创建的模块");
|
||||
module.setSort(0);
|
||||
return module;
|
||||
}
|
||||
|
||||
private ObjectStatusTransitionDO createTransition(String actionCode, String toStatus, boolean needReason) {
|
||||
ObjectStatusTransitionDO transition = new ObjectStatusTransitionDO();
|
||||
transition.setActionCode(actionCode);
|
||||
transition.setToStatusCode(toStatus);
|
||||
transition.setNeedReason(needReason);
|
||||
return transition;
|
||||
}
|
||||
|
||||
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId, String nickname) {
|
||||
MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
|
||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserNickname).thenReturn(nickname);
|
||||
return mockedStatic;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -66,6 +66,43 @@ class ProductServiceImplTest extends BaseMockitoUnitTest {
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Mock
|
||||
private AdminUserApi adminUserApi;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper requirementModuleMapper;
|
||||
|
||||
@Test
|
||||
void createProduct_shouldCreateDefaultRequirementModule() {
|
||||
Long loginUserId = 3001L;
|
||||
ProductSaveReqVO reqVO = new ProductSaveReqVO();
|
||||
reqVO.setDirectionCode("direction_value");
|
||||
reqVO.setName("新产品");
|
||||
reqVO.setManagerUserId(2001L);
|
||||
reqVO.setDescription("产品描述");
|
||||
|
||||
when(productMapper.selectByName("新产品")).thenReturn(null);
|
||||
when(adminUserApi.getUser(2001L)).thenReturn(
|
||||
success(new com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO()));
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
productService.createProduct(reqVO);
|
||||
}
|
||||
|
||||
// 验证产品已创建
|
||||
ArgumentCaptor<ProductDO> productCaptor = ArgumentCaptor.forClass(ProductDO.class);
|
||||
verify(productMapper, times(1)).insert(productCaptor.capture());
|
||||
ProductDO createdProduct = productCaptor.getValue();
|
||||
assertEquals("新产品", createdProduct.getName());
|
||||
|
||||
// 验证"全部需求"模块已自动创建
|
||||
ArgumentCaptor<com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO> moduleCaptor =
|
||||
ArgumentCaptor.forClass(com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO.class);
|
||||
verify(requirementModuleMapper, times(1)).insert(moduleCaptor.capture());
|
||||
com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO defaultModule = moduleCaptor.getValue();
|
||||
assertEquals(0L, defaultModule.getParentId());
|
||||
assertEquals(createdProduct.getId(), defaultModule.getProductId());
|
||||
assertEquals("全部需求", defaultModule.getModuleName());
|
||||
assertEquals("自动创建的模块", defaultModule.getRemark());
|
||||
assertEquals(0, defaultModule.getSort());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateProductBaseInfo_shouldOnlyUpdateBaseInfoAndRecordFieldChanges() {
|
||||
|
||||
Reference in New Issue
Block a user