refactor(project): 重构权限常量定义并移除需求进度聚合功能

- 将产品和项目查询权限码统一提取到常量类中
- 移除需求进度聚合计算的相关实现代码
- 更新权限验证注解使用新的常量定义
- 清理相关的单元测试代码
- 更新错误码注释说明
This commit is contained in:
2026-06-08 17:17:18 +08:00
parent d7c52e12d7
commit 10b7ccdeb0
8 changed files with 20 additions and 260 deletions

View File

@@ -128,8 +128,7 @@ public interface ErrorCodeConstants {
ErrorCode PROJECT_EXECUTION_ASSIGNEE_ALREADY_EXISTS = new ErrorCode(1_008_003_004, "该用户已是当前执行的有效协办人");
ErrorCode PROJECT_EXECUTION_ASSIGNEE_NOT_EXISTS = new ErrorCode(1_008_003_005, "执行协办人不存在");
ErrorCode PROJECT_EXECUTION_ASSIGNEE_NOT_ACTIVE = new ErrorCode(1_008_003_006, "当前执行协办人已失效");
// 保留TD-013 解锁后业务路径已不会再触发,预留用于灰度回滚关闭关联能力
ErrorCode PROJECT_EXECUTION_REQUIREMENT_NOT_READY = new ErrorCode(1_008_003_007, "当前阶段不支持给执行绑定项目需求");
// 1_008_003_007 原 PROJECT_EXECUTION_REQUIREMENT_NOT_READY 已随 TD-013 清理删除,号位保留空缺不复用,避免与历史前端映射冲突
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_008, "当前项目状态不允许维护执行");
ErrorCode PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED = new ErrorCode(1_008_003_009, "该项目成员仍担任未终态执行负责人,请先完成执行负责人交接");
ErrorCode PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_003_010, "执行状态定义不存在或已停用");

View File

@@ -28,6 +28,11 @@ public final class ProductObjectConstants {
*/
public static final String CODE_PREFIX = "CNPD";
/**
* 产品查询权限码。
*/
public static final String PERMISSION_QUERY = "project:product:query";
/**
* 产品编辑权限码。
*/

View File

@@ -40,6 +40,11 @@ public final class ProjectObjectConstants {
*/
public static final String CODE_PREFIX = "CNPJ";
/**
* 项目查询权限码。
*/
public static final String PERMISSION_QUERY = "project:project:query";
/**
* 项目编辑权限码。
*/

View File

@@ -97,10 +97,8 @@ public class ProjectRequirementRespVO {
@Schema(description = "是否为终态", example = "false")
private Boolean terminal;
@Schema(description = "需求进度TD-016 读时聚合service 层批量计算)。"
+ "公式AVG(该需求自己承接的执行进度 直接子需求进度)"
+ "排除 rdms_object_status_model.progress_excluded_flag=1 的执行状态(当前为 cancelled"
+ "无任何执行且无子需求时返回 0.00。两位小数HALF_UP。",
@Schema(description = "需求进度TD-016读时聚合计算已下线(前端当前不展示需求进度,进度仅项目/执行/任务展示),"
+ "字段保留占位、当前恒为 null未来需要展示需求进度时再恢复服务端聚合计算。",
example = "0.65")
private BigDecimal progressRate;

View File

@@ -76,11 +76,11 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
// 权限常量
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_QUERY_PERMISSION = ProductObjectConstants.PERMISSION_QUERY;
private static final String PRODUCT_UPDATE_PERMISSION = ProductObjectConstants.PERMISSION_UPDATE;
private static final String PRODUCT_STATUS_PERMISSION = ProductObjectConstants.PERMISSION_STATUS;
private static final String PRODUCT_REVIEW_PERMISSION = ProductObjectConstants.PERMISSION_REVIEW;
private static final String PRODUCT_DELETE_PERMISSION = "project:product:delete";
private static final String PRODUCT_DELETE_PERMISSION = ProductObjectConstants.PERMISSION_DELETE;
private static final String PRODUCT_SPLIT_PERMISSION = "project:product:split";
// 审计动作常量

View File

@@ -88,7 +88,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
STATUS_REJECTED, STATUS_CANCELLED);
private static final String PROJECT_CREATE_PERMISSION = "project:project:create";
private static final String PROJECT_QUERY_PERMISSION = "project:project:query";
private static final String PROJECT_QUERY_PERMISSION = ProjectObjectConstants.PERMISSION_QUERY;
private static final String PROJECT_UPDATE_PERMISSION = ProjectObjectConstants.PERMISSION_UPDATE;
private static final String PROJECT_STATUS_PERMISSION = ProjectObjectConstants.PERMISSION_STATUS;
private static final String PROJECT_REVIEW_PERMISSION = ProjectObjectConstants.PERMISSION_REVIEW;
@@ -219,7 +219,6 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
ProjectRequirementDO requirement = validateRequirementExists(id);
validateRequirementBelongsToProject(requirement, projectId);
ProjectRequirementRespVO respVO = buildRequirementRespVO(requirement);
fillRequirementProgress(projectId, List.of(respVO));
return respVO;
}
@@ -241,7 +240,6 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
List<ProjectRequirementRespVO> list = pageResult.getList().stream()
.map(requirement -> buildRequirementRespVO(requirement, statusModelMap))
.collect(Collectors.toList());
fillRequirementProgress(pageReqVO.getProjectId(), list);
return new PageResult<>(list, pageResult.getTotal());
}
@@ -296,7 +294,6 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
List<ProjectRequirementRespVO> list = pagedRootRequirements.stream()
.map(requirement -> buildRequirementRespVOWithPathChildren(requirement, pathNodeIds, childrenMap, statusModelMap))
.collect(Collectors.toList());
fillRequirementProgress(projectId, list);
return new PageResult<>(list, (long) total);
}
@@ -1134,124 +1131,6 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
return respVO;
}
/**
* TD-016把项目需求 RespVO 列表上的 progressRate 字段批量回填。
* <p>口径:按 projectId 一次性算出本项目下所有需求的进度 map再递归扫 RespVO 树(含 children应用。
* 公式R.progressRate = AVG(R 自己挂的执行进度 R 的直接子需求进度),排除进度排除字典命中的执行状态;
* 当 pool 为空(无承接执行且无子需求)时返回 0.00。两位小数 HALF_UP。
*/
private void fillRequirementProgress(Long projectId, Collection<ProjectRequirementRespVO> respVOList) {
if (respVOList == null || respVOList.isEmpty() || projectId == null) {
return;
}
Map<Long, BigDecimal> progressMap = computeRequirementProgressMapByProjectId(projectId);
applyProgressRecursive(respVOList, progressMap);
}
private void applyProgressRecursive(Collection<ProjectRequirementRespVO> list, Map<Long, BigDecimal> progressMap) {
BigDecimal defaultValue = normalizeProgress(BigDecimal.ZERO);
for (ProjectRequirementRespVO vo : list) {
vo.setProgressRate(progressMap.getOrDefault(vo.getId(), defaultValue));
if (vo.getChildren() != null && !vo.getChildren().isEmpty()) {
applyProgressRecursive(vo.getChildren(), progressMap);
}
}
}
/**
* 按 projectId 算所有需求进度,返回 Map&lt;requirementId, progressRate&gt;。
* 公式与 fillRequirementProgress 对齐:自下而上 DFSpool = 自己挂的执行平均 直接子需求进度。
*/
@VisibleForTesting
Map<Long, BigDecimal> computeRequirementProgressMapByProjectId(Long projectId) {
List<ProjectRequirementDO> allRequirements = requirementMapper.selectList(
new LambdaQueryWrapperX<ProjectRequirementDO>()
.eq(ProjectRequirementDO::getProjectId, projectId));
if (allRequirements.isEmpty()) {
return Collections.emptyMap();
}
Set<Long> requirementIds = allRequirements.stream()
.map(ProjectRequirementDO::getId)
.collect(Collectors.toCollection(LinkedHashSet::new));
// 一次 GROUP BY 拉到所有需求"自己挂的执行平均进度"
List<String> excludedStatusCodes = loadProgressExcludedExecutionStatusCodes();
Map<Long, BigDecimal> ownAvgMap = new HashMap<>();
List<Map<String, Object>> rows = projectExecutionMapper
.selectAvgProgressGroupByProjectRequirementIds(requirementIds, excludedStatusCodes);
if (rows != null) {
for (Map<String, Object> row : rows) {
Object idObj = row.get("projectRequirementId");
Object progressObj = row.get("progressRate");
if (idObj == null || progressObj == null) {
continue;
}
Long reqId = ((Number) idObj).longValue();
BigDecimal avg = progressObj instanceof BigDecimal
? (BigDecimal) progressObj
: new BigDecimal(progressObj.toString());
ownAvgMap.put(reqId, avg);
}
}
// 按 parentId 建子需求索引(顶级父 id = 0
Map<Long, List<ProjectRequirementDO>> childrenIndex = allRequirements.stream()
.collect(Collectors.groupingBy(r -> r.getParentId() == null ? 0L : r.getParentId()));
// DFS 自下而上递归
Map<Long, BigDecimal> result = new HashMap<>();
for (ProjectRequirementDO r : allRequirements) {
if (!result.containsKey(r.getId())) {
computeProgressDfs(r, childrenIndex, ownAvgMap, result);
}
}
return result;
}
private BigDecimal computeProgressDfs(ProjectRequirementDO r,
Map<Long, List<ProjectRequirementDO>> childrenIndex,
Map<Long, BigDecimal> ownAvgMap,
Map<Long, BigDecimal> result) {
if (result.containsKey(r.getId())) {
return result.get(r.getId());
}
List<BigDecimal> pool = new ArrayList<>();
BigDecimal ownAvg = ownAvgMap.get(r.getId());
if (ownAvg != null) {
pool.add(ownAvg);
}
List<ProjectRequirementDO> children = childrenIndex.getOrDefault(r.getId(), List.of());
for (ProjectRequirementDO child : children) {
pool.add(computeProgressDfs(child, childrenIndex, ownAvgMap, result));
}
BigDecimal value;
if (pool.isEmpty()) {
value = normalizeProgress(BigDecimal.ZERO);
} else {
BigDecimal sum = pool.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
value = sum.divide(BigDecimal.valueOf(pool.size()), 2, RoundingMode.HALF_UP);
}
result.put(r.getId(), value);
return value;
}
/**
* 从 rdms_object_status_model 字典动态读取执行的"进度排除状态"列表,
* 当前命中 cancelled任何时候运维通过 progress_excluded_flag 增减service 层无需重新部署。
*/
private List<String> loadProgressExcludedExecutionStatusCodes() {
List<String> codes = statusModelMapper
.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
return codes == null ? Collections.emptyList() : codes;
}
private BigDecimal normalizeProgress(BigDecimal value) {
if (value == null) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
return value.setScale(2, RoundingMode.HALF_UP);
}
private List<ProjectRequirementModuleRespVO> buildModuleTree(List<ProjectRequirementModuleDO> modules, Long parentId) {
return modules.stream()
.filter(module -> Objects.equals(module.getParentId(), parentId))

View File

@@ -79,7 +79,7 @@ public class RequirementReviewServiceImpl implements RequirementReviewService {
@Override
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
permission = "project:product:query")
permission = ProductObjectConstants.PERMISSION_QUERY)
public RequirementReviewRespVO getProductRequirementReview(Long productId, Long requirementId) {
ProductRequirementDO requirement = productRequirementMapper.selectById(requirementId);
if (requirement == null || !Objects.equals(requirement.getProductId(), productId)) {
@@ -117,7 +117,7 @@ public class RequirementReviewServiceImpl implements RequirementReviewService {
@Override
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
permission = "project:project:query")
permission = ProjectObjectConstants.PERMISSION_QUERY)
public RequirementReviewRespVO getProjectRequirementReview(Long projectId, Long requirementId) {
ProjectRequirementDO requirement = projectRequirementMapper.selectById(requirementId);
if (requirement == null || !Objects.equals(requirement.getProjectId(), projectId)) {

View File

@@ -129,130 +129,4 @@ class ProjectRequirementServiceImplTest extends BaseMockitoUnitTest {
return requirement;
}
// ============== TD-016 进度聚合 ==============
/**
* TD-016叶子需求无承接执行 → 进度 0.00。
*/
@Test
void computeRequirementProgress_whenLeafHasNoExecution_shouldBeZero() {
Long projectId = 2001L;
ProjectRequirementDO leaf = buildRequirementWithParent(9001L, projectId, "implementing", 0L);
when(requirementMapper.selectList(any(com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX.class)))
.thenReturn(List.of(leaf));
when(statusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE))
.thenReturn(List.of("cancelled"));
when(projectExecutionMapper.selectAvgProgressGroupByProjectRequirementIds(any(), any()))
.thenReturn(List.of());
Map<Long, BigDecimal> result = projectRequirementService.computeRequirementProgressMapByProjectId(projectId);
assertEquals(new BigDecimal("0.00"), result.get(9001L));
}
/**
* TD-016叶子需求有 N 个执行AVG(progress_rate) 即结果。
* SQL 层已经 GROUP BY 平均service 只是把那个平均值放回 pool 平均pool 大小 1 等于自身)
*/
@Test
void computeRequirementProgress_whenLeafHasExecutions_shouldUseAvg() {
Long projectId = 2001L;
ProjectRequirementDO leaf = buildRequirementWithParent(9001L, projectId, "implementing", 0L);
when(requirementMapper.selectList(any(com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX.class)))
.thenReturn(List.of(leaf));
when(statusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE))
.thenReturn(List.of("cancelled"));
when(projectExecutionMapper.selectAvgProgressGroupByProjectRequirementIds(any(), any()))
.thenReturn(List.of(rowOf(9001L, "0.60")));
Map<Long, BigDecimal> result = projectRequirementService.computeRequirementProgressMapByProjectId(projectId);
assertEquals(new BigDecimal("0.60"), result.get(9001L));
}
/**
* TD-016父需求 = AVG(自己挂的执行的平均, 每个直接子需求进度)。
* 例:父 9000 自己挂的执行平均 0.40;子 9001叶子执行平均 0.80
* 父 = AVG(0.40, 0.80) = 0.60。子 9001 = 0.80。
*/
@Test
void computeRequirementProgress_whenParentHasOwnExecutionsAndChildren_shouldAveragePool() {
Long projectId = 2001L;
ProjectRequirementDO parent = buildRequirementWithParent(9000L, projectId, "implementing", 0L);
ProjectRequirementDO child = buildRequirementWithParent(9001L, projectId, "implementing", 9000L);
when(requirementMapper.selectList(any(com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX.class)))
.thenReturn(List.of(parent, child));
when(statusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE))
.thenReturn(List.of("cancelled"));
when(projectExecutionMapper.selectAvgProgressGroupByProjectRequirementIds(any(), any()))
.thenReturn(List.of(rowOf(9000L, "0.40"), rowOf(9001L, "0.80")));
Map<Long, BigDecimal> result = projectRequirementService.computeRequirementProgressMapByProjectId(projectId);
assertEquals(new BigDecimal("0.80"), result.get(9001L));
assertEquals(new BigDecimal("0.60"), result.get(9000L));
}
/**
* TD-016父需求无自挂执行、有 2 个子需求 → 父 = AVG(子1, 子2)。
*/
@Test
void computeRequirementProgress_whenParentHasNoOwnExecutionPureChildren_shouldAverageChildren() {
Long projectId = 2001L;
ProjectRequirementDO parent = buildRequirementWithParent(9000L, projectId, "implementing", 0L);
ProjectRequirementDO child1 = buildRequirementWithParent(9001L, projectId, "implementing", 9000L);
ProjectRequirementDO child2 = buildRequirementWithParent(9002L, projectId, "implementing", 9000L);
when(requirementMapper.selectList(any(com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX.class)))
.thenReturn(List.of(parent, child1, child2));
when(statusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE))
.thenReturn(List.of("cancelled"));
when(projectExecutionMapper.selectAvgProgressGroupByProjectRequirementIds(any(), any()))
.thenReturn(List.of(rowOf(9001L, "0.50"), rowOf(9002L, "1.00")));
Map<Long, BigDecimal> result = projectRequirementService.computeRequirementProgressMapByProjectId(projectId);
assertEquals(new BigDecimal("0.50"), result.get(9001L));
assertEquals(new BigDecimal("1.00"), result.get(9002L));
assertEquals(new BigDecimal("0.75"), result.get(9000L));
}
/**
* TD-016三层结构。爷父(9000) → 父(9100) → 叶子(9101),每层都挂执行;
* 验证自下而上递归正确。
* 叶子 9101 自挂执行 0.80 → 9101 = 0.80
* 父 9100 自挂执行 0.40 + 子 0.80 → AVG = 0.60
* 爷 9000 自挂执行 0.20 + 子 0.60 → AVG = 0.40
*/
@Test
void computeRequirementProgress_multiLevel_shouldRecurseFromBottomUp() {
Long projectId = 2001L;
ProjectRequirementDO grand = buildRequirementWithParent(9000L, projectId, "implementing", 0L);
ProjectRequirementDO parent = buildRequirementWithParent(9100L, projectId, "implementing", 9000L);
ProjectRequirementDO leaf = buildRequirementWithParent(9101L, projectId, "implementing", 9100L);
when(requirementMapper.selectList(any(com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX.class)))
.thenReturn(List.of(grand, parent, leaf));
when(statusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE))
.thenReturn(List.of("cancelled"));
when(projectExecutionMapper.selectAvgProgressGroupByProjectRequirementIds(any(), any()))
.thenReturn(List.of(rowOf(9000L, "0.20"), rowOf(9100L, "0.40"), rowOf(9101L, "0.80")));
Map<Long, BigDecimal> result = projectRequirementService.computeRequirementProgressMapByProjectId(projectId);
assertEquals(new BigDecimal("0.80"), result.get(9101L));
assertEquals(new BigDecimal("0.60"), result.get(9100L));
assertEquals(new BigDecimal("0.40"), result.get(9000L));
}
private ProjectRequirementDO buildRequirementWithParent(Long id, Long projectId, String statusCode, Long parentId) {
ProjectRequirementDO requirement = buildRequirement(id, projectId, statusCode);
requirement.setParentId(parentId);
return requirement;
}
private Map<String, Object> rowOf(Long requirementId, String avg) {
Map<String, Object> row = new HashMap<>();
row.put("projectRequirementId", requirementId);
row.put("progressRate", new BigDecimal(avg));
return row;
}
}