feat(project): 重构项目产品概览统计响应模型支持动态状态看板
- 引入 StatusBoardItemVO 类封装状态看板项,包含状态编码、名称、数量、排序等字段 - 添加 total 字段表示「全部」口径总数,items 字段存储状态看板项列表 - 更新 ProductOverviewSummaryRespVO 和 ProjectOverviewSummaryRespVO 结构 - 移除 ProjectObjectConstants 中的已归档状态常量和默认查询排除状态码集合 - 重写 buildStatusBoardItems 方法实现状态看板项动态构建逻辑 - 调整项目「全部」口径定义,将已取消和已归档状态重新计入统计范围 - 更新相关单元测试验证新字段和统计口径的正确性 - 修改项目分组接口的状态筛选逻辑,使其与新的统计口径保持一致
This commit is contained in:
@@ -35,19 +35,6 @@ public final class ProjectObjectConstants {
|
||||
*/
|
||||
public static final String STATUS_CANCELLED = "cancelled";
|
||||
|
||||
/**
|
||||
* 已归档项目状态编码。
|
||||
*/
|
||||
public static final String STATUS_ARCHIVED = "archived";
|
||||
|
||||
/**
|
||||
* "全部"口径的排除锚点:列表/统计缺省不含 已取消、已归档。
|
||||
* 状态全集以 rdms_object_status_model 为权威源运行时推导(DB 新增状态自动进入"全部"口径),
|
||||
* 代码只锚定排除项,与主线唯一性校验的 STATUS_CANCELLED 同属最小硬编码。
|
||||
* 设计来源:docs/superpowers/specs/2026-06-10-项目列表产品分组-design.md 第二节。
|
||||
*/
|
||||
public static final Set<String> DEFAULT_QUERY_EXCLUDED_STATUS_CODES = Set.of(STATUS_CANCELLED, STATUS_ARCHIVED);
|
||||
|
||||
/**
|
||||
* 项目自动编码前缀。
|
||||
*/
|
||||
|
||||
@@ -3,13 +3,39 @@ package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "管理后台 - 产品入口页概览统计 Response VO")
|
||||
@Data
|
||||
public class ProductOverviewSummaryRespVO {
|
||||
|
||||
@Schema(description = "产品状态数量,按当前启用的产品状态模型返回")
|
||||
@Schema(description = "产品状态数量,按当前启用的产品状态模型返回(兼容旧前端的过渡字段,前端改造完成后移除)")
|
||||
private Map<String, Long> statusCounts;
|
||||
|
||||
@Schema(description = "「全部」口径总数 = items 各状态 count 之和(产品域当前无「全部」视图,口径同项目域:启用状态全集)", example = "12")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "状态看板项列表,覆盖状态机全部启用状态,按 sort 升序;状态机新增启用状态自动进入清单")
|
||||
private List<StatusBoardItemVO> items;
|
||||
|
||||
@Schema(description = "产品状态看板项")
|
||||
@Data
|
||||
public static class StatusBoardItemVO {
|
||||
|
||||
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||
private String statusCode;
|
||||
@Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "启用")
|
||||
private String statusName;
|
||||
@Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||
private Long count;
|
||||
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Integer sort;
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
@Schema(description = "是否计入「全部」(当前口径无排除项,恒为 true)", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
private Boolean includeInAll;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,16 +3,42 @@ package com.njcn.rdms.module.project.controller.admin.project.vo.project;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "管理后台 - 项目入口页概览统计 Response VO")
|
||||
@Data
|
||||
public class ProjectOverviewSummaryRespVO {
|
||||
|
||||
@Schema(description = "项目状态数量,按当前启用的项目状态模型返回")
|
||||
@Schema(description = "项目状态数量,按当前启用的项目状态模型返回(兼容旧前端的过渡字段,前端改造完成后移除)")
|
||||
private Map<String, Long> statusCounts;
|
||||
|
||||
@Schema(description = "游离项目数:未挂产品且状态属于「全部」口径(状态机启用状态,不含已取消/已归档)")
|
||||
@Schema(description = "「全部」口径总数 = items 各状态 count 之和(2026-06-11 起「全部」= 启用状态全集,作废/归档计入)", example = "48")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "状态看板项列表,覆盖状态机全部启用状态,按 sort 升序;状态机新增启用状态自动进入清单")
|
||||
private List<StatusBoardItemVO> items;
|
||||
|
||||
@Schema(description = "游离项目数:未挂产品且状态属于「全部」口径(状态机启用状态全集)")
|
||||
private Long orphanCount;
|
||||
|
||||
@Schema(description = "项目状态看板项")
|
||||
@Data
|
||||
public static class StatusBoardItemVO {
|
||||
|
||||
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||
private String statusCode;
|
||||
@Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中")
|
||||
private String statusName;
|
||||
@Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
|
||||
private Long count;
|
||||
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
|
||||
private Integer sort;
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
@Schema(description = "是否计入「全部」(当前口径无排除项,恒为 true)", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
private Boolean includeInAll;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -383,10 +383,15 @@ public class ProductServiceImpl implements ProductService {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
|
||||
List<Map<String, Object>> rows = buildStatusCountRows(scope);
|
||||
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
|
||||
.selectListByObjectType(ProductObjectConstants.OBJECT_TYPE);
|
||||
|
||||
ProductOverviewSummaryRespVO respVO = new ProductOverviewSummaryRespVO();
|
||||
respVO.setStatusCounts(buildProductStatusCounts(objectStatusModelMapper
|
||||
.selectListByObjectType(ProductObjectConstants.OBJECT_TYPE), rows));
|
||||
Map<String, Long> statusCounts = buildProductStatusCounts(statusModels, rows);
|
||||
respVO.setStatusCounts(statusCounts);
|
||||
respVO.setItems(buildStatusBoardItems(statusModels, statusCounts));
|
||||
respVO.setTotal(respVO.getItems().stream()
|
||||
.mapToLong(ProductOverviewSummaryRespVO.StatusBoardItemVO::getCount).sum());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@@ -756,6 +761,31 @@ public class ProductServiceImpl implements ProductService {
|
||||
return statusCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态看板项:清单由状态机启用状态运行时推导,过滤/排序口径与 buildProductStatusCounts 一致(sort 升序)。
|
||||
* 产品域当前无「全部」视图,口径与项目域保持同构(无排除项),includeInAll 恒为 true(设计来源:
|
||||
* docs/superpowers/specs/2026-06-11-项目产品列表状态看板动态化-design.md 第二节)。
|
||||
*/
|
||||
private List<ProductOverviewSummaryRespVO.StatusBoardItemVO> buildStatusBoardItems(
|
||||
List<ObjectStatusModelDO> statusModels, Map<String, Long> statusCounts) {
|
||||
return statusModels.stream()
|
||||
.filter(statusModel -> Objects.equals(statusModel.getStatus(), 0))
|
||||
.filter(statusModel -> StringUtils.hasText(statusModel.getStatusCode()))
|
||||
.sorted(Comparator.comparing(ObjectStatusModelDO::getSort, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(ObjectStatusModelDO::getStatusCode))
|
||||
.map(statusModel -> {
|
||||
ProductOverviewSummaryRespVO.StatusBoardItemVO item = new ProductOverviewSummaryRespVO.StatusBoardItemVO();
|
||||
item.setStatusCode(statusModel.getStatusCode());
|
||||
item.setStatusName(statusModel.getStatusName());
|
||||
item.setCount(statusCounts.getOrDefault(statusModel.getStatusCode(), 0L));
|
||||
item.setSort(statusModel.getSort());
|
||||
item.setTerminal(statusModel.getTerminalFlag());
|
||||
item.setIncludeInAll(true);
|
||||
return item;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void writeProductStatusLog(ProductDO product, String actionType, String fromStatus,
|
||||
String toStatus, String reason) {
|
||||
ProductStatusLogDO statusLog = new ProductStatusLogDO();
|
||||
|
||||
@@ -503,16 +503,21 @@ class ProjectServiceImpl implements ProjectService {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProjectObjectConstants.OBJECT_TYPE);
|
||||
List<Map<String, Object>> rows = buildStatusCountRows(scope);
|
||||
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
|
||||
.selectListByObjectType(ProjectObjectConstants.OBJECT_TYPE);
|
||||
|
||||
ProjectOverviewSummaryRespVO respVO = new ProjectOverviewSummaryRespVO();
|
||||
respVO.setStatusCounts(buildProjectStatusCounts(objectStatusModelMapper
|
||||
.selectListByObjectType(ProjectObjectConstants.OBJECT_TYPE), rows));
|
||||
Map<String, Long> statusCounts = buildProjectStatusCounts(statusModels, rows);
|
||||
respVO.setStatusCounts(statusCounts);
|
||||
respVO.setItems(buildStatusBoardItems(statusModels, statusCounts));
|
||||
respVO.setTotal(respVO.getItems().stream()
|
||||
.mapToLong(ProjectOverviewSummaryRespVO.StatusBoardItemVO::getCount).sum());
|
||||
respVO.setOrphanCount(countOrphanProjects(scope));
|
||||
return respVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游离项目计数:未挂产品 + "全部"口径(与项目分组列表对齐)。
|
||||
* 游离项目计数:未挂产品 + "全部"口径(启用状态全集,与项目分组列表对齐)。
|
||||
*/
|
||||
private Long countOrphanProjects(ObjectDataScope scope) {
|
||||
if (scope.getState() == ObjectDataScope.State.EMPTY) {
|
||||
@@ -530,14 +535,13 @@ class ProjectServiceImpl implements ProjectService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目"全部"口径状态集:状态机启用状态全集 - {已取消, 已归档}。
|
||||
* 项目"全部"口径状态集:状态机启用状态全集(2026-06-11 口径变更:作废/归档同样计入,无排除项)。
|
||||
* 权威源 rdms_object_status_model,DB 新增状态自动纳入,代码不维护正向清单。
|
||||
*/
|
||||
private Set<String> resolveDefaultProjectStatusCodes() {
|
||||
return objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE).stream()
|
||||
.map(ObjectStatusModelDO::getStatusCode)
|
||||
.filter(StringUtils::hasText)
|
||||
.filter(code -> !ProjectObjectConstants.DEFAULT_QUERY_EXCLUDED_STATUS_CODES.contains(code))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@@ -559,11 +563,11 @@ class ProjectServiceImpl implements ProjectService {
|
||||
ObjectDataScope projectScope = objectDataScopeService.compute(loginUserId, ProjectObjectConstants.OBJECT_TYPE);
|
||||
ObjectDataScope productScope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
|
||||
|
||||
// 状态口径:"全部"集合由状态机推导(typeCounts 恒按该口径,无论是否显式传状态都需要)
|
||||
Set<String> defaultStatusCodes = resolveDefaultProjectStatusCodes();
|
||||
// 状态口径:显式传 statusCode 按单状态过滤,不传时"全部"集合由状态机推导;
|
||||
// typeCounts 与 projectTotal/projects 同用此状态集(2026-06-11 口径变更:随 statusCode 联动)
|
||||
Collection<String> statusCodes = StringUtils.hasText(reqVO.getStatusCode())
|
||||
? List.of(reqVO.getStatusCode())
|
||||
: defaultStatusCodes;
|
||||
: resolveDefaultProjectStatusCodes();
|
||||
|
||||
// 1. 命中项目全量加载(产品几十级、项目百级,内存分组成本可忽略;项目上千后再改 SQL 聚合分页)
|
||||
List<ProjectDO> matched = selectGroupMatchedProjects(reqVO, statusCodes, projectScope);
|
||||
@@ -618,12 +622,13 @@ class ProjectServiceImpl implements ProjectService {
|
||||
List<Long> pageProductIds = orderedProductIds.subList(fromIndex, Math.min(toIndex, orderedProductIds.size()));
|
||||
boolean orphanOnPage = hasOrphanGroup && toIndex == totalGroups;
|
||||
|
||||
// 8. typeCounts(恒按"全部"口径)与 hasBaseline(非已取消主线):产品级属性,不随筛选与数据权限变化
|
||||
// 8. typeCounts(与 projectTotal 同口径,随 statusCode 联动;keyword/projectType/数据权限仍不影响)
|
||||
// 与 hasBaseline(恒按"全部"口径的非已取消主线,不随筛选变化)
|
||||
Map<Long, Map<String, Long>> typeCountsMap = new HashMap<>();
|
||||
Map<String, Long> orphanTypeCounts = new HashMap<>();
|
||||
if ((!pageProductIds.isEmpty() || orphanOnPage) && !defaultStatusCodes.isEmpty()) {
|
||||
if ((!pageProductIds.isEmpty() || orphanOnPage) && !statusCodes.isEmpty()) {
|
||||
LambdaQueryWrapperX<ProjectDO> typeWrapper = new LambdaQueryWrapperX<ProjectDO>()
|
||||
.in(ProjectDO::getStatusCode, defaultStatusCodes);
|
||||
.in(ProjectDO::getStatusCode, statusCodes);
|
||||
boolean withOrphan = orphanOnPage;
|
||||
typeWrapper.and(w -> {
|
||||
if (!pageProductIds.isEmpty()) {
|
||||
@@ -1554,6 +1559,31 @@ class ProjectServiceImpl implements ProjectService {
|
||||
return statusCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态看板项:清单由状态机启用状态运行时推导,过滤/排序口径与 buildProjectStatusCounts 一致(sort 升序)。
|
||||
* 「全部」口径无排除项(作废/归档同样计入),includeInAll 恒为 true(设计来源:
|
||||
* docs/superpowers/specs/2026-06-11-项目产品列表状态看板动态化-design.md 第二节)。
|
||||
*/
|
||||
private List<ProjectOverviewSummaryRespVO.StatusBoardItemVO> buildStatusBoardItems(
|
||||
List<ObjectStatusModelDO> statusModels, Map<String, Long> statusCounts) {
|
||||
return statusModels.stream()
|
||||
.filter(statusModel -> Objects.equals(statusModel.getStatus(), 0))
|
||||
.filter(statusModel -> StringUtils.hasText(statusModel.getStatusCode()))
|
||||
.sorted(Comparator.comparing(ObjectStatusModelDO::getSort, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(ObjectStatusModelDO::getStatusCode))
|
||||
.map(statusModel -> {
|
||||
ProjectOverviewSummaryRespVO.StatusBoardItemVO item = new ProjectOverviewSummaryRespVO.StatusBoardItemVO();
|
||||
item.setStatusCode(statusModel.getStatusCode());
|
||||
item.setStatusName(statusModel.getStatusName());
|
||||
item.setCount(statusCounts.getOrDefault(statusModel.getStatusCode(), 0L));
|
||||
item.setSort(statusModel.getSort());
|
||||
item.setTerminal(statusModel.getTerminalFlag());
|
||||
item.setIncludeInAll(true);
|
||||
return item;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void validateDeleteConfirmText(String confirmText) {
|
||||
String normalizedConfirmText = normalizeNullableText(confirmText);
|
||||
if (!Objects.equals(ProjectObjectConstants.DELETE_CONFIRM_TEXT, normalizedConfirmText)) {
|
||||
|
||||
@@ -229,24 +229,38 @@ class ProductServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@Test
|
||||
void getProductOverviewSummary_shouldReturnFixedStatusCountsAndIgnoreUnknownStatus() {
|
||||
when(objectStatusModelMapper.selectListByObjectType("product")).thenReturn(List.of(
|
||||
createStatusModel("active", 10, 0),
|
||||
createStatusModel("paused", 20, 0),
|
||||
createStatusModel("abandoned", 30, 0),
|
||||
createStatusModel("disabled", 40, 1)
|
||||
));
|
||||
when(productMapper.selectStatusCountList()).thenReturn(List.of(
|
||||
Map.of("statusCode", "active", "countValue", 5L),
|
||||
Map.of("statusCode", "abandoned", "countValue", 1L),
|
||||
Map.of("statusCode", "unknown", "countValue", 99L)
|
||||
));
|
||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1L);
|
||||
when(objectDataScopeService.compute(1L, "product")).thenReturn(ObjectDataScope.all());
|
||||
when(objectStatusModelMapper.selectListByObjectType("product")).thenReturn(List.of(
|
||||
createBoardStatusModel("active", "启用", 10, 0, false),
|
||||
createBoardStatusModel("paused", "暂停", 20, 0, false),
|
||||
createBoardStatusModel("abandoned", "废弃", 30, 0, true),
|
||||
createBoardStatusModel("disabled", "已禁用", 40, 1, false)
|
||||
));
|
||||
when(productMapper.selectStatusCountList()).thenReturn(List.of(
|
||||
Map.of("statusCode", "active", "countValue", 5L),
|
||||
Map.of("statusCode", "abandoned", "countValue", 1L),
|
||||
Map.of("statusCode", "unknown", "countValue", 99L)
|
||||
));
|
||||
|
||||
ProductOverviewSummaryRespVO respVO = productService.getProductOverviewSummary();
|
||||
ProductOverviewSummaryRespVO respVO = productService.getProductOverviewSummary();
|
||||
|
||||
assertEquals(3, respVO.getStatusCounts().size());
|
||||
assertEquals(5L, respVO.getStatusCounts().get("active"));
|
||||
assertEquals(0L, respVO.getStatusCounts().get("paused"));
|
||||
assertEquals(1L, respVO.getStatusCounts().get("abandoned"));
|
||||
// 旧字段兼容:仅启用状态进 statusCounts,未知状态忽略
|
||||
assertEquals(3, respVO.getStatusCounts().size());
|
||||
assertEquals(5L, respVO.getStatusCounts().get("active"));
|
||||
assertEquals(0L, respVO.getStatusCounts().get("paused"));
|
||||
assertEquals(1L, respVO.getStatusCounts().get("abandoned"));
|
||||
// 新状态看板:仅启用状态、按 sort 升序、includeInAll 恒 true
|
||||
assertEquals(3, respVO.getItems().size());
|
||||
assertEquals("active", respVO.getItems().get(0).getStatusCode());
|
||||
assertEquals("启用", respVO.getItems().get(0).getStatusName());
|
||||
assertEquals(5L, respVO.getItems().get(0).getCount());
|
||||
assertEquals(Boolean.TRUE, respVO.getItems().get(0).getIncludeInAll());
|
||||
assertEquals("abandoned", respVO.getItems().get(2).getStatusCode());
|
||||
assertEquals(Boolean.TRUE, respVO.getItems().get(2).getTerminal());
|
||||
assertEquals(6L, respVO.getTotal()); // 5+0+1
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -658,12 +672,16 @@ class ProductServiceImplTest extends BaseMockitoUnitTest {
|
||||
return statusModel;
|
||||
}
|
||||
|
||||
private ObjectStatusModelDO createStatusModel(String statusCode, Integer sort, Integer status) {
|
||||
/** 状态看板测试用状态机行:带展示名 / 终态标识。 */
|
||||
private ObjectStatusModelDO createBoardStatusModel(String statusCode, String statusName,
|
||||
int sort, int status, boolean terminalFlag) {
|
||||
ObjectStatusModelDO statusModel = new ObjectStatusModelDO();
|
||||
statusModel.setObjectType("product");
|
||||
statusModel.setStatusCode(statusCode);
|
||||
statusModel.setStatusName(statusName);
|
||||
statusModel.setSort(sort);
|
||||
statusModel.setStatus(status);
|
||||
statusModel.setTerminalFlag(terminalFlag);
|
||||
return statusModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
@Mock
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper projectRequirementModuleMapper;
|
||||
@Mock
|
||||
private ProjectStatusViewService projectStatusViewService;
|
||||
@Mock
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@@ -208,24 +212,43 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@Test
|
||||
void getProjectOverviewSummary_shouldReturnFixedStatusCountsAndIgnoreUnknownStatus() {
|
||||
when(objectStatusModelMapper.selectListByObjectType("project")).thenReturn(List.of(
|
||||
createStatusModel("pending", 10, 0),
|
||||
createStatusModel("active", 20, 0),
|
||||
createStatusModel("paused", 30, 0),
|
||||
createStatusModel("disabled", 40, 1)
|
||||
));
|
||||
when(projectMapper.selectStatusCountList()).thenReturn(List.of(
|
||||
Map.of("statusCode", "pending", "countValue", 2L),
|
||||
Map.of("statusCode", "active", "countValue", 3L),
|
||||
Map.of("statusCode", "unknown", "countValue", 99L)
|
||||
));
|
||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1L);
|
||||
when(objectDataScopeService.compute(1L, "project")).thenReturn(ObjectDataScope.all());
|
||||
when(objectStatusModelMapper.selectListByObjectType("project")).thenReturn(List.of(
|
||||
createBoardStatusModel("pending", "待开始", 10, 0, false),
|
||||
createBoardStatusModel("active", "进行中", 20, 0, false),
|
||||
createBoardStatusModel("archived", "已归档", 60, 0, true),
|
||||
createBoardStatusModel("disabled", "已禁用", 70, 1, false)
|
||||
));
|
||||
when(projectMapper.selectStatusCountList()).thenReturn(List.of(
|
||||
Map.of("statusCode", "pending", "countValue", 2L),
|
||||
Map.of("statusCode", "active", "countValue", 3L),
|
||||
Map.of("statusCode", "archived", "countValue", 4L),
|
||||
Map.of("statusCode", "unknown", "countValue", 99L)
|
||||
));
|
||||
when(objectStatusModelMapper.selectListByObjectTypeEnabled("project")).thenReturn(projectStatusModels());
|
||||
when(projectMapper.selectCount(any(LambdaQueryWrapperX.class))).thenReturn(1L);
|
||||
|
||||
ProjectOverviewSummaryRespVO respVO = projectService.getProjectOverviewSummary();
|
||||
ProjectOverviewSummaryRespVO respVO = projectService.getProjectOverviewSummary();
|
||||
|
||||
assertEquals(3, respVO.getStatusCounts().size());
|
||||
assertEquals(2L, respVO.getStatusCounts().get("pending"));
|
||||
assertEquals(3L, respVO.getStatusCounts().get("active"));
|
||||
assertEquals(0L, respVO.getStatusCounts().get("paused"));
|
||||
// 旧字段兼容:仅启用状态进 statusCounts,未知状态忽略
|
||||
assertEquals(3, respVO.getStatusCounts().size());
|
||||
assertEquals(2L, respVO.getStatusCounts().get("pending"));
|
||||
assertEquals(3L, respVO.getStatusCounts().get("active"));
|
||||
assertEquals(4L, respVO.getStatusCounts().get("archived"));
|
||||
// 新状态看板:仅启用状态、按 sort 升序、归档计入「全部」、includeInAll 恒 true
|
||||
assertEquals(3, respVO.getItems().size());
|
||||
assertEquals("pending", respVO.getItems().get(0).getStatusCode());
|
||||
assertEquals("待开始", respVO.getItems().get(0).getStatusName());
|
||||
assertEquals(2L, respVO.getItems().get(0).getCount());
|
||||
assertEquals(Boolean.FALSE, respVO.getItems().get(0).getTerminal());
|
||||
assertEquals(Boolean.TRUE, respVO.getItems().get(0).getIncludeInAll());
|
||||
assertEquals("archived", respVO.getItems().get(2).getStatusCode());
|
||||
assertEquals(Boolean.TRUE, respVO.getItems().get(2).getTerminal());
|
||||
assertEquals(9L, respVO.getTotal()); // 2+3+4,归档计入「全部」总数
|
||||
assertEquals(1L, respVO.getOrphanCount());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -563,7 +586,7 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
ProjectDO p3 = createGroupProject(103L, 11L, "contract", LocalDateTime.of(2026, 6, 2, 10, 0));
|
||||
ProjectDO p4 = createGroupProject(104L, 12L, "tech_support", LocalDateTime.of(2026, 6, 1, 10, 0));
|
||||
ProjectDO p5 = createGroupProject(105L, null, "contract", LocalDateTime.of(2026, 6, 1, 10, 0));
|
||||
// selectList 调用次序与实现一致:① 命中项目 ② typeCounts(四状态) ③ hasBaseline(主线)
|
||||
// selectList 调用次序与实现一致:① 命中项目 ② typeCounts(不传 statusCode 时按"全部"口径) ③ hasBaseline(主线)
|
||||
when(projectMapper.selectList(any(LambdaQueryWrapperX.class)))
|
||||
.thenReturn(List.of(p1, p2, p3, p4, p5),
|
||||
List.of(p1, p2, p3, p4, p5),
|
||||
@@ -633,7 +656,7 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1L);
|
||||
when(objectDataScopeService.compute(1L, "project")).thenReturn(ObjectDataScope.all());
|
||||
when(objectDataScopeService.compute(1L, "product")).thenReturn(ObjectDataScope.all());
|
||||
when(objectStatusModelMapper.selectListByObjectTypeEnabled("project")).thenReturn(projectStatusModels());
|
||||
// 显式传 statusCode 时不再推导项目"全部"状态集,仅 stub 产品状态机
|
||||
when(objectStatusModelMapper.selectListByObjectTypeEnabled("product")).thenReturn(productStatusModels());
|
||||
|
||||
ProjectDO p1 = createGroupProject(101L, 11L, "contract", LocalDateTime.of(2026, 6, 1, 10, 0));
|
||||
@@ -649,6 +672,11 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
assertEquals(1L, respVO.getTotal()); // 仅产品 11;产品 12 不补占位;游离无命中不返回
|
||||
assertEquals(11L, respVO.getList().get(0).getProductId());
|
||||
assertEquals(Map.of("contract", 1L), respVO.getList().get(0).getTypeCounts());
|
||||
|
||||
// typeCounts 与 projectTotal 同口径:传 statusCode 时全程不推导项目"全部"状态集,
|
||||
// 即 typeCounts 统计不可能回退到"全部"口径
|
||||
verify(objectStatusModelMapper, never()).selectListByObjectTypeEnabled("project");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,15 +854,6 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
return statusModel;
|
||||
}
|
||||
|
||||
private ObjectStatusModelDO createStatusModel(String statusCode, Integer sort, Integer status) {
|
||||
ObjectStatusModelDO statusModel = new ObjectStatusModelDO();
|
||||
statusModel.setObjectType("project");
|
||||
statusModel.setStatusCode(statusCode);
|
||||
statusModel.setSort(sort);
|
||||
statusModel.setStatus(status);
|
||||
return statusModel;
|
||||
}
|
||||
|
||||
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId, String nickname) {
|
||||
MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
|
||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||
@@ -842,6 +861,19 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
return mockedStatic;
|
||||
}
|
||||
|
||||
/** 状态看板测试用状态机行:带展示名 / 终态标识。 */
|
||||
private ObjectStatusModelDO createBoardStatusModel(String statusCode, String statusName,
|
||||
int sort, int status, boolean terminalFlag) {
|
||||
ObjectStatusModelDO statusModel = new ObjectStatusModelDO();
|
||||
statusModel.setObjectType("project");
|
||||
statusModel.setStatusCode(statusCode);
|
||||
statusModel.setStatusName(statusName);
|
||||
statusModel.setSort(sort);
|
||||
statusModel.setStatus(status);
|
||||
statusModel.setTerminalFlag(terminalFlag);
|
||||
return statusModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组接口测试用状态机行:status=0(启用)。"全部"口径与产品存续状态均由状态机推导,测试必须配齐。
|
||||
*/
|
||||
@@ -854,7 +886,7 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
return statusModel;
|
||||
}
|
||||
|
||||
/** 项目域状态机 stub:启用状态全集(含 cancelled/archived,推导逻辑应将其排除)。 */
|
||||
/** 项目域状态机 stub:启用状态全集(含 cancelled/archived;2026-06-11 起「全部」口径不再排除)。 */
|
||||
private List<ObjectStatusModelDO> projectStatusModels() {
|
||||
return List.of(
|
||||
createGroupStatusModel("project", "pending", false),
|
||||
|
||||
Reference in New Issue
Block a user