feat(产品需求): 产品和产品需求相关的测试类

This commit is contained in:
dk
2026-04-28 16:53:33 +08:00
parent 9ad7e063c0
commit b4e1aae062
21 changed files with 1951 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
package com.njcn.rdms.module.project.controller.admin.product;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementCloseReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementLifecycleRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementModuleRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementModuleSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementPageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementSplitReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementStatusTransitionRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementUpdateReqVO;
import com.njcn.rdms.module.project.service.product.ProductRequirementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
/**
* 管理后台 - 产品需求控制器
*/
@Tag(name = "管理后台 - 产品需求")
@RestController
@RequestMapping("/project/product/requirement")
@Validated
public class ProductRequirementController {
@Resource
private ProductRequirementService requirementService;
// ========== 需求管理 ==========
@PostMapping("/create")
@Operation(summary = "创建产品需求")
public CommonResult<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));
}
@GetMapping("/page")
@Operation(summary = "获取需求分页列表")
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementPage(@Valid ProductRequirementPageReqVO pageReqVO) {
return success(requirementService.getRequirementPage(pageReqVO));
}
@GetMapping("/tree")
@Operation(summary = "获取需求树形列表")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
@Parameter(name = "moduleId", description = "模块编号", example = "1024")
public CommonResult<List<ProductRequirementRespVO>> getRequirementTree(@RequestParam("productId") Long productId,
@RequestParam(value = "moduleId", required = false) Long moduleId) {
return success(requirementService.getRequirementTree(productId, moduleId));
}
@PostMapping("/change-status")
@Operation(summary = "变更需求状态")
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) {
requirementService.changeRequirementStatus(reqVO);
return success(true);
}
@PostMapping("/delete")
@Operation(summary = "删除产品需求")
@Parameter(name = "id", description = "需求编号", required = true, example = "1024")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<Boolean> deleteRequirement(@RequestParam("id") Long id,
@RequestParam("productId") Long productId) {
requirementService.deleteRequirement(id, productId);
return success(true);
}
@PostMapping("/split")
@Operation(summary = "拆分产品需求")
public CommonResult<Long> splitRequirement(@Valid @RequestBody ProductRequirementSplitReqVO 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 ProductRequirementModuleSaveReqVO reqVO) {
return success(requirementService.createRequirementModule(reqVO));
}
@PutMapping("/module/update")
@Operation(summary = "更新需求模块")
public CommonResult<Boolean> updateRequirementModule(@Valid @RequestBody ProductRequirementModuleSaveReqVO reqVO) {
requirementService.updateRequirementModule(reqVO);
return success(true);
}
@PostMapping("/module/delete")
@Operation(summary = "删除需求模块")
@Parameter(name = "moduleId", description = "模块编号", required = true, example = "1024")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<Boolean> deleteRequirementModule(@RequestParam("moduleId") Long moduleId,
@RequestParam("productId") Long productId) {
requirementService.deleteRequirementModule(moduleId, productId);
return success(true);
}
@GetMapping("/module/tree")
@Operation(summary = "获取需求模块树")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<List<ProductRequirementModuleRespVO>> getRequirementModuleTree(@RequestParam("productId") Long productId) {
return success(requirementService.getRequirementModuleTree(productId));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 = "父模块ID0表示顶级", 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;
}

View File

@@ -0,0 +1,40 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 管理后台 - 产品需求模块保存 Request VO
*/
@Schema(description = "管理后台 - 产品需求模块保存 Request VO")
@Data
public class ProductRequirementModuleSaveReqVO {
@Schema(description = "模块ID编辑时传入", example = "1024")
private Long id;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品ID不能为空")
private Long productId;
@Schema(description = "父模块ID0表示顶级", example = "0")
private Long parentId;
@Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能")
@NotBlank(message = "模块名称不能为空")
@Size(max = 100, message = "模块名称长度不能超过100个字符")
private String moduleName;
@Schema(description = "模块说明", example = "产品核心功能模块")
private String remark;
@Schema(description = "图标", example = "icon-function")
private String icon;
@Schema(description = "排序值", example = "0")
private Integer sort;
}

View File

@@ -0,0 +1,43 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 管理后台 - 产品需求分页 Request VO
*/
@Schema(description = "管理后台 - 产品需求分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProductRequirementPageReqVO extends PageParam {
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long productId;
@Schema(description = "所属模块ID", example = "1024")
private Long moduleId;
@Schema(description = "父需求ID查询子需求时使用", example = "1024")
private Long parentId;
@Schema(description = "标题关键词", example = "模块")
private String title;
@Schema(description = "需求分类字典值", example = "function")
private String category;
@Schema(description = "优先级0低 1中 2高 3紧急", example = "1")
private Integer priority;
@Schema(description = "状态编码", example = "pending_dispatch")
private String statusCode;
@Schema(description = "当前处理人用户编号", example = "1024")
private Long currentHandlerUserId;
@Schema(description = "来源类型manual:手工新增, work_order:工单流转)", example = "manual")
private String sourceType;
}

View File

@@ -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 = "父需求ID0表示顶级需求", 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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,59 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 管理后台 - 产品需求拆分 Request VO
*/
@Schema(description = "管理后台 - 产品需求拆分 Request VO")
@Data
public class ProductRequirementSplitReqVO {
@Schema(description = "父需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "父需求编号不能为空")
private Long parentId;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品编号不能为空")
private Long productId;
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
@NotBlank(message = "需求标题不能为空")
@Size(max = 200, message = "需求标题长度不能超过200个字符")
private String title;
@Schema(description = "需求描述(富文本)", example = "<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 = "当前处理人用户编号", example = "1024")
private Long currentHandlerUserId;
@Schema(description = "默认实现项目编号", example = "1024")
private Long implementProjectId;
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "预期完成时间不能为空")
private LocalDateTime completionDate;
@Schema(description = "排序值(越小越靠前)", example = "0")
private Integer sort;
@Schema(description = "拆分原因", example = "需求过大,需要拆分实现")
private String splitReason;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 管理后台 - 产品需求状态变更 Request VO
* 注意:需求不直接关联产品,通过模块间接关联
*/
@Schema(description = "管理后台 - 产品需求状态变更 Request VO")
@Data
public class ProductRequirementStatusActionReqVO {
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "需求ID不能为空")
private Long id;
@Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "dispatch")
@NotBlank(message = "动作编码不能为空")
@Size(max = 32, message = "动作编码长度不能超过32个字符")
private String actionCode;
@Schema(description = "状态变更原因", example = "评审通过,进入分流阶段")
private String reason;
@Schema(description = "实现项目编号dispatch动作时可选", example = "1024")
private Long implementProjectId;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
/**
* 父需求ID0表示顶级需求
*/
private Long parentId;
/**
* 所属模块ID0表示全部需求
*/
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;
}

View File

@@ -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;
/**
* 父模块ID0表示顶级
*/
private Long parentId;
/**
* 所属产品ID
*/
private Long productId;
/**
* 模块名称
*/
private String moduleName;
/**
* 模块说明
*/
private String remark;
/**
* 图标
*/
private String icon;
/**
* 排序值
*/
private Integer sort;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,110 @@
package com.njcn.rdms.module.project.dal.mysql.product;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.ProductRequirementPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 产品需求 Mapper
*/
@Mapper
public interface ProductRequirementMapper extends BaseMapperX<ProductRequirementDO> {
/**
* 分页查询需求列表
*/
default PageResult<ProductRequirementDO> selectPage(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精确匹配
.eqIfPresent(ProductRequirementDO::getModuleId, reqVO.getModuleId())
// 父需求ID精确匹配查询子需求时使用
.eqIfPresent(ProductRequirementDO::getParentId, reqVO.getParentId())
// 产品ID精确匹配
.eq(ProductRequirementDO::getProductId, reqVO.getProductId())
// 按排序值升序,再按创建时间降序
.orderByAsc(ProductRequirementDO::getSort)
.orderByDesc(ProductRequirementDO::getCreateTime);
return selectPage(reqVO, queryWrapper);
}
/**
* 根据产品ID和模块ID查询需求列表
*/
default List<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) {
ProductRequirementDO update = new ProductRequirementDO();
update.setStatusCode(toStatus);
update.setLastStatusReason(lastStatusReason);
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));
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,134 @@
package com.njcn.rdms.module.project.service.product;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
import java.util.List;
/**
* 产品需求 Service 接口
*/
public interface ProductRequirementService {
/**
* 创建产品需求
*
* @param createReqVO 创建请求
* @return 需求编号
*/
Long createRequirement(ProductRequirementSaveReqVO createReqVO);
/**
* 更新产品需求(不含状态变更)
*
* @param updateReqVO 更新请求
*/
void updateRequirement(ProductRequirementUpdateReqVO updateReqVO);
/**
* 获取需求详情
*
* @param id 需求编号
* @return 需求详情
*/
ProductRequirementRespVO getRequirement(Long id);
/**
* 获取需求分页列表
*
* @param pageReqVO 分页请求
* @return 分页结果
*/
PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO);
/**
* 获取需求树形列表(包含子需求)
*
* @param productId 产品编号
* @param moduleId 模块编号可为null
* @return 需求树形列表
*/
List<ProductRequirementRespVO> getRequirementTree(Long productId, Long moduleId);
/**
* 变更需求状态
*
* @param reqVO 状态动作请求
*/
void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO);
/**
* 删除需求
*
* @param id 需求编号
* @param productId 产品编号
*/
void deleteRequirement(Long id, Long productId);
/**
* 拆分需求(创建子需求)
*
* @param reqVO 拆分请求
* @return 子需求编号
*/
Long splitRequirement(ProductRequirementSplitReqVO reqVO);
/**
* 关闭需求(大需求关闭时级联关闭子需求)
*
* @param reqVO 关闭请求
*/
void closeRequirement(ProductRequirementCloseReqVO reqVO);
/**
* 获取需求当前可执行的状态动作列表
*
* @param requirementId 需求编号
* @param productId 产品编号
* @return 可执行动作列表
*/
List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId);
/**
* 获取需求生命周期信息(当前状态 + 可执行动作)
*
* @param requirementId 需求编号
* @param productId 产品编号
* @return 生命周期信息
*/
ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId);
// ========== 模块管理 ==========
/**
* 创建需求模块
*
* @param reqVO 模块保存请求
* @return 模块编号
*/
Long createRequirementModule(ProductRequirementModuleSaveReqVO reqVO);
/**
* 更新需求模块
*
* @param reqVO 模块保存请求
*/
void updateRequirementModule(ProductRequirementModuleSaveReqVO reqVO);
/**
* 删除需求模块(级联删除模块下需求)
*
* @param moduleId 模块编号
* @param productId 产品编号
*/
void deleteRequirementModule(Long moduleId, Long productId);
/**
* 获取产品需求模块树
*
* @param productId 产品编号
* @return 模块树
*/
List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId);
}

View File

@@ -0,0 +1,710 @@
package com.njcn.rdms.module.project.service.product;
import com.google.common.annotations.VisibleForTesting;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
/**
* 产品需求 Service 实现类
*/
@Service
public class ProductRequirementServiceImpl implements ProductRequirementService {
// 对象类型常量
private static final String REQUIREMENT_OBJECT_TYPE = "product_requirement";
private static final String PRODUCT_OBJECT_TYPE = "product";
// 需求状态常量
private static final String STATUS_PENDING_CONFIRM = "pending_confirm";
private static final String STATUS_PENDING_REVIEW = "pending_review";
private static final String STATUS_PENDING_DISPATCH = "pending_dispatch";
private static final String STATUS_IMPLEMENTING = "implementing";
private static final String STATUS_ACCEPTED = "accepted";
private static final String STATUS_CLOSED = "closed";
private static final String STATUS_REJECTED = "rejected";
private static final String STATUS_CANCELLED = "cancelled";
// 终态状态集合
private static final List<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 String PRODUCT_QUERY_PERMISSION = "project:product:query";
private static final String PRODUCT_UPDATE_PERMISSION = "project:product:update";
// 审计动作常量
private static final String ACTION_CREATE = "create";
private static final String ACTION_UPDATE = "update";
private static final String ACTION_DELETE = "delete";
private static final String ACTION_SPLIT = "split";
private static final String ACTION_CLOSE = "close";
private static final String BIZ_TYPE_REQUIREMENT = "product_requirement";
@Resource
private ProductRequirementMapper requirementMapper;
@Resource
private ProductRequirementModuleMapper moduleMapper;
@Resource
private ProductRequirementStatusLogMapper statusLogMapper;
@Resource
private BizAuditLogMapper bizAuditLogMapper;
@Resource
private ObjectStatusTransitionMapper statusTransitionMapper;
@Resource
private ObjectStatusModelMapper statusModelMapper;
// ========== 需求增删改查 ==========
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#createReqVO.productId",
permission = PRODUCT_UPDATE_PERMISSION)
public Long createRequirement(ProductRequirementSaveReqVO createReqVO) {
// 当未选择模块时,自动归属到该产品的"全部需求"模块
Long moduleId = resolveModuleId(createReqVO.getModuleId(), createReqVO.getProductId());
// 校验模块是否属于当前产品
validateModuleBelongsToProduct(moduleId, createReqVO.getProductId());
ProductRequirementDO requirement = new ProductRequirementDO();
requirement.setProductId(createReqVO.getProductId());
requirement.setParentId(0L); // 新增需求默认是顶级需求
requirement.setModuleId(moduleId);
requirement.setReviewRequired(createReqVO.getReviewRequired());
requirement.setTitle(createReqVO.getTitle().trim());
requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
requirement.setCategory(createReqVO.getCategory());
requirement.setSourceType("manual"); // 手工新增默认来源类型
requirement.setPriority(createReqVO.getPriority());
// 根据是否需要评审确定初始状态
String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
requirement.setStatusCode(initialStatus);
requirement.setProposerId(createReqVO.getProposerId());
requirement.setCurrentHandlerUserId(createReqVO.getCurrentHandlerUserId());
requirement.setImplementProjectId(createReqVO.getImplementProjectId());
requirement.setCompletionDate(createReqVO.getCompletionDate());
requirement.setSort(createReqVO.getSort() != null ? createReqVO.getSort() : 0);
requirementMapper.insert(requirement);
// 写入业务审计日志
writeBizAuditLog(requirement, ACTION_CREATE, null, initialStatus, null, null);
return requirement.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#updateReqVO.productId",
permission = PRODUCT_UPDATE_PERMISSION)
public void updateRequirement(ProductRequirementUpdateReqVO updateReqVO) {
ProductRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
// 校验终态不允许编辑
validateRequirementEditable(requirement);
// 当未选择模块时,自动归属到该产品的"全部需求"模块
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProductId());
// 校验模块是否属于当前产品
validateModuleBelongsToProduct(moduleId, updateReqVO.getProductId());
String fromStatus = requirement.getStatusCode();
requirement.setModuleId(moduleId);
requirement.setReviewRequired(updateReqVO.getReviewRequired());
requirement.setTitle(updateReqVO.getTitle().trim());
requirement.setDescription(normalizeNullableText(updateReqVO.getDescription()));
requirement.setCategory(updateReqVO.getCategory());
requirement.setPriority(updateReqVO.getPriority());
requirement.setProposerId(updateReqVO.getProposerId());
requirement.setCurrentHandlerUserId(updateReqVO.getCurrentHandlerUserId());
requirement.setImplementProjectId(updateReqVO.getImplementProjectId());
requirement.setCompletionDate(updateReqVO.getCompletionDate());
requirement.setSort(updateReqVO.getSort() != null ? updateReqVO.getSort() : 0);
requirementMapper.updateById(requirement);
writeBizAuditLog(requirement, ACTION_UPDATE, fromStatus, requirement.getStatusCode(), null, null);
}
@Override
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_QUERY_PERMISSION)
public ProductRequirementRespVO getRequirement(Long id) {
ProductRequirementDO requirement = validateRequirementExists(id);
return buildRequirementRespVO(requirement);
}
@Override
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#pageReqVO.productId",
permission = PRODUCT_QUERY_PERMISSION)
public PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO) {
// 判断是否查询"全部需求"模块通过查询模块的parentId是否为0L来判断
if (pageReqVO.getModuleId() != null && isAllRequirementsModule(pageReqVO.getModuleId())) {
// "全部需求"模块忽略模块ID条件查询该产品下所有需求
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 = "#productId",
permission = PRODUCT_QUERY_PERMISSION)
public List<ProductRequirementRespVO> getRequirementTree(Long productId, Long moduleId) {
// 查询当前产品下的所有顶级需求
ProductRequirementPageReqVO pageReqVO = new ProductRequirementPageReqVO();
pageReqVO.setProductId(productId);
pageReqVO.setParentId(0L);
pageReqVO.setPageSize(1000); // 树形查询取较大值
// 判断是否查询"全部需求"模块通过查询模块的parentId是否为0L来判断
if (moduleId != null && !isAllRequirementsModule(moduleId)) {
// 非"全部需求"模块添加模块ID过滤条件
pageReqVO.setModuleId(moduleId);
}
// 如果是"全部需求"模块不设置moduleId条件查询该产品下所有需求
List<ProductRequirementDO> parentList = requirementMapper.selectPage(pageReqVO).getList();
// 构建树形结构
return parentList.stream()
.map(this::buildRequirementRespVOWithChildren)
.collect(Collectors.toList());
}
/**
* 判断指定模块是否为"全部需求"模块parentId = 0L 的根模块)
*/
@VisibleForTesting
boolean isAllRequirementsModule(Long moduleId) {
if (moduleId == null) {
return false;
}
ProductRequirementModuleDO module = moduleMapper.selectById(moduleId);
return module != null && module.getParentId() != null && module.getParentId() == 0L;
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
permission = PRODUCT_UPDATE_PERMISSION)
public void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
String actionCode = reqVO.getActionCode().trim();
String fromStatus = requirement.getStatusCode();
// 校验状态流转是否合法
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
String reason = normalizeNullableText(reqVO.getReason());
// 校验是否需要填写原因
validateTransitionReason(transition, reason);
String toStatus = transition.getToStatusCode();
// dispatch动作时更新实现项目
if ("dispatch".equals(actionCode) && reqVO.getImplementProjectId() != null) {
requirement.setImplementProjectId(reqVO.getImplementProjectId());
}
// 带并发控制的状态更新
int updateCount = requirementMapper.updateStatusByIdAndStatus(requirement.getId(), fromStatus, toStatus, reason);
if (updateCount != 1) {
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
}
requirement.setStatusCode(toStatus);
requirement.setLastStatusReason(reason);
// 写入状态变更日志
writeRequirementStatusLog(requirement, actionCode, fromStatus, toStatus, reason);
// 写入业务审计日志
writeBizAuditLog(requirement, actionCode, fromStatus, toStatus, null, reason);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_UPDATE_PERMISSION)
public void deleteRequirement(Long id, Long productId) {
ProductRequirementDO requirement = validateRequirementExists(id);
String fromStatus = requirement.getStatusCode();
// 带并发控制的删除(以当前状态作为条件)
int deleteCount = requirementMapper.deleteByIdAndStatus(id, fromStatus);
if (deleteCount != 1) {
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
}
writeBizAuditLog(requirement, ACTION_DELETE, fromStatus, null, null, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
permission = PRODUCT_UPDATE_PERMISSION)
public Long splitRequirement(ProductRequirementSplitReqVO reqVO) {
// 校验父需求是否存在
ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId());
// 校验父需求状态是否允许拆分(只能是待分流或实施中)
validateParentAllowSplit(parentRequirement);
// 创建子需求
ProductRequirementDO childRequirement = new ProductRequirementDO();
childRequirement.setParentId(reqVO.getParentId());
childRequirement.setModuleId(parentRequirement.getModuleId()); // 子需求继承父需求的模块
childRequirement.setReviewRequired(0); // 子需求默认不需要评审
childRequirement.setTitle(reqVO.getTitle().trim());
childRequirement.setDescription(normalizeNullableText(reqVO.getDescription()));
childRequirement.setCategory(reqVO.getCategory());
childRequirement.setSourceType(parentRequirement.getSourceType()); // 继承父需求来源类型
childRequirement.setPriority(reqVO.getPriority());
// 子需求初始状态为待分流
childRequirement.setStatusCode(STATUS_PENDING_DISPATCH);
childRequirement.setProposerId(parentRequirement.getProposerId()); // 继承父需求提出人
childRequirement.setCurrentHandlerUserId(reqVO.getCurrentHandlerUserId());
childRequirement.setImplementProjectId(reqVO.getImplementProjectId());
childRequirement.setCompletionDate(reqVO.getCompletionDate());
childRequirement.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
requirementMapper.insert(childRequirement);
// 父需求状态从待分流变为实施中
if (STATUS_PENDING_DISPATCH.equals(parentRequirement.getStatusCode())) {
String fromStatus = parentRequirement.getStatusCode();
int updateCount = requirementMapper.updateStatusByIdAndStatus(
parentRequirement.getId(), fromStatus, STATUS_IMPLEMENTING, null);
if (updateCount == 1) {
parentRequirement.setStatusCode(STATUS_IMPLEMENTING);
writeRequirementStatusLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null);
writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null, null);
}
}
// 写入子需求的业务审计日志
writeBizAuditLog(childRequirement, ACTION_CREATE, null, STATUS_PENDING_DISPATCH, null, null);
return childRequirement.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
permission = PRODUCT_UPDATE_PERMISSION)
public void closeRequirement(ProductRequirementCloseReqVO reqVO) {
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
String fromStatus = requirement.getStatusCode();
String reason = reqVO.getReason().trim();
// 校验当前状态是否为已验收(只有已验收才能关闭)
if (!STATUS_ACCEPTED.equals(fromStatus)) {
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_CLOSE);
}
// 如果是父需求,校验所有子需求是否允许关闭
List<ProductRequirementDO> children = requirementMapper.selectListByParentId(requirement.getId());
if (!children.isEmpty()) {
for (ProductRequirementDO child : children) {
if (!CHILD_ALLOW_CLOSE_STATUSES.contains(child.getStatusCode())) {
throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
}
}
// 将所有已验收的子需求关闭
for (ProductRequirementDO child : children) {
if (STATUS_ACCEPTED.equals(child.getStatusCode())) {
int updateCount = requirementMapper.updateStatusByIdAndStatus(
child.getId(), STATUS_ACCEPTED, STATUS_CLOSED, reason);
if (updateCount == 1) {
writeRequirementStatusLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, reason);
writeBizAuditLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, null, reason);
}
}
}
}
// 关闭当前需求
int updateCount = requirementMapper.updateStatusByIdAndStatus(
requirement.getId(), fromStatus, STATUS_CLOSED, reason);
if (updateCount != 1) {
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
}
requirement.setStatusCode(STATUS_CLOSED);
requirement.setLastStatusReason(reason);
writeRequirementStatusLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, reason);
writeBizAuditLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, null, reason);
}
@Override
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_QUERY_PERMISSION)
public List<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_UPDATE_PERMISSION)
public Long createRequirementModule(ProductRequirementModuleSaveReqVO reqVO) {
// 校验模块名称在同一产品下是否唯一
validateModuleNameUnique(reqVO.getProductId(), null, reqVO.getModuleName());
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
module.setParentId(reqVO.getParentId() != null ? reqVO.getParentId() : 0L);
module.setProductId(reqVO.getProductId());
module.setModuleName(reqVO.getModuleName().trim());
module.setRemark(normalizeNullableText(reqVO.getRemark()));
module.setIcon(normalizeNullableText(reqVO.getIcon()));
module.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
moduleMapper.insert(module);
return module.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
permission = PRODUCT_UPDATE_PERMISSION)
public void updateRequirementModule(ProductRequirementModuleSaveReqVO reqVO) {
if (reqVO.getId() == null) {
throw invalidParamException("模块编号不能为空");
}
ProductRequirementModuleDO module = validateModuleExists(reqVO.getId());
// 校验模块名称唯一性
validateModuleNameUnique(reqVO.getProductId(), reqVO.getId(), reqVO.getModuleName());
module.setModuleName(reqVO.getModuleName().trim());
module.setRemark(normalizeNullableText(reqVO.getRemark()));
module.setIcon(normalizeNullableText(reqVO.getIcon()));
module.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
moduleMapper.updateById(module);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_UPDATE_PERMISSION)
public void deleteRequirementModule(Long moduleId, Long productId) {
ProductRequirementModuleDO module = validateModuleExists(moduleId);
// 查询模块下的所有需求
List<ProductRequirementDO> requirements = requirementMapper.selectListByModuleId(moduleId);
// 校验是否存在非终态需求
for (ProductRequirementDO requirement : requirements) {
if (!TERMINAL_STATUSES.contains(requirement.getStatusCode())) {
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS);
}
}
// 级联软删除模块下的所有需求
for (ProductRequirementDO requirement : requirements) {
requirementMapper.deleteById(requirement.getId());
}
// 删除模块
moduleMapper.deleteById(moduleId);
}
@Override
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_QUERY_PERMISSION)
public List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId) {
List<ProductRequirementModuleDO> modules = moduleMapper.selectListByProductId(productId);
return buildModuleTree(modules, 0L);
}
// ========== 私有辅助方法 ==========
/**
* 构建需求响应VO不含子需求
*/
private ProductRequirementRespVO buildRequirementRespVO(ProductRequirementDO requirement) {
ProductRequirementRespVO respVO = BeanUtils.toBean(requirement, ProductRequirementRespVO.class);
// 查询状态名称
ObjectStatusModelDO statusModel = statusModelMapper
.selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode());
if (statusModel != null) {
respVO.setStatusName(statusModel.getStatusName());
respVO.setTerminal(statusModel.getTerminalFlag());
}
// 设置是否终态
if (respVO.getTerminal() == null) {
respVO.setTerminal(TERMINAL_STATUSES.contains(requirement.getStatusCode()));
}
return respVO;
}
/**
* 构建需求响应VO含子需求
*/
private ProductRequirementRespVO buildRequirementRespVOWithChildren(ProductRequirementDO requirement) {
ProductRequirementRespVO respVO = buildRequirementRespVO(requirement);
// 查询子需求
List<ProductRequirementDO> children = requirementMapper.selectListByParentId(requirement.getId());
if (!children.isEmpty()) {
respVO.setChildren(children.stream()
.map(this::buildRequirementRespVO)
.collect(Collectors.toList()));
}
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 (moduleId == null || !existModule.getId().equals(moduleId)) {
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NAME_DUPLICATE, moduleName.trim());
}
}
/**
* 解析模块ID当未选择模块时自动归属到该产品的"全部需求"模块
* "全部需求"模块的标志性特征是 parentId = 0L
*/
@VisibleForTesting
Long resolveModuleId(Long moduleId, Long productId) {
if (moduleId != null) {
return moduleId;
}
// 查询该产品的"全部需求"模块parentId = 0L 的根模块)
ProductRequirementModuleDO defaultModule = moduleMapper
.selectByProductIdAndParentId(productId, 0L);
if (defaultModule == null) {
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
}
return defaultModule.getId();
}
/**
* 校验模块是否属于当前产品
*/
@VisibleForTesting
void validateModuleBelongsToProduct(Long moduleId, Long productId) {
if (moduleId == null) {
return; // 全部需求模块不需要校验
}
ProductRequirementModuleDO module = moduleMapper.selectById(moduleId);
if (module == null) {
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
}
if (!Objects.equals(module.getProductId(), productId)) {
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_BELONG_TO_PRODUCT);
}
}
/**
* 写入需求状态变更日志
*/
private void writeRequirementStatusLog(ProductRequirementDO requirement, String actionType,
String fromStatus, String toStatus, String reason) {
ProductRequirementStatusLogDO statusLog = new ProductRequirementStatusLogDO();
statusLog.setRequirementId(requirement.getId());
statusLog.setActionType(actionType);
statusLog.setFromStatus(fromStatus);
statusLog.setToStatus(toStatus);
statusLog.setReason(defaultText(reason));
statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
statusLog.setRequirementTitleSnapshot(requirement.getTitle());
statusLogMapper.insert(statusLog);
}
/**
* 写入业务审计日志
*/
private void writeBizAuditLog(ProductRequirementDO requirement, String actionType, String fromStatus,
String toStatus, String fieldChanges, String reason) {
BizAuditLogDO auditLog = new BizAuditLogDO();
auditLog.setBizType(BIZ_TYPE_REQUIREMENT);
auditLog.setBizId(requirement.getId());
auditLog.setActionType(actionType);
auditLog.setFromStatus(fromStatus);
auditLog.setToStatus(toStatus);
auditLog.setFieldChanges(fieldChanges);
auditLog.setReason(reason);
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
bizAuditLogMapper.insert(auditLog);
}
/**
* 处理可能为空的文本去除首尾空格后若为空则返回null
*/
private String normalizeNullableText(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
/**
* 处理可能为空的文本,若为空则返回空字符串
*/
private String defaultText(String value) {
return StringUtils.hasText(value) ? value : "";
}
}