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)) {
|
||||
|
||||
Reference in New Issue
Block a user