feat(project): 添加项目任务跨执行聚合查询功能

- 新增 ObjectPermissionService.hasPermission 非抛模式权限检查方法
- 实现 ProjectObjectPermissionService.hasPermission 权限验证逻辑
- 为 ProductObjectPermissionService 预留空实现并添加日志警告
- 在 ProjectExecutionController 中支持负数 pageSize 查询全部功能
- 添加 ProjectTaskConstants.PERMISSION_LIST_ALL 权限码常量定义
- 扩展 ProjectTaskMapper 支持跨执行聚合分页、状态统计和摘要查询
- 更新 ProjectTaskRespVO 包含执行名称和状态码字段
- 实现 ProjectTaskService.assembleTaskRespVOPageCrossExecution 跨执行装配方法
- 优化任务服务中的执行信息批量回填和生命周期应用逻辑
- 统一使用服务器时区 Asia/Shanghai 处理日期时间操作
- 为 .claude 设置添加新的代码搜索和分析命令
This commit is contained in:
2026-05-23 14:18:15 +08:00
parent 8a36b49128
commit df13a90107
9 changed files with 637 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
package com.njcn.rdms.module.project.controller.admin.project.task;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateBoardPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryRespVO;
import com.njcn.rdms.module.project.service.project.task.ProjectTaskAggregateService;
import io.swagger.v3.oas.annotations.Operation;
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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 项目级跨执行任务查询")
@RestController
@RequestMapping("/project/project/{projectId}/tasks")
@Validated
public class ProjectTaskAggregateController {
@Resource
private ProjectTaskAggregateService projectTaskAggregateService;
@GetMapping("/page")
@Operation(summary = "获取项目级跨执行任务分页")
public CommonResult<PageResult<ProjectTaskRespVO>> getTaskPage(
@PathVariable("projectId") Long projectId,
@Valid ProjectTaskAggregatePageReqVO reqVO) {
return success(projectTaskAggregateService.getAggregateTaskPage(projectId, reqVO));
}
@GetMapping("/status-board")
@Operation(summary = "获取项目级跨执行任务状态看板")
public CommonResult<ProjectTaskStatusBoardRespVO> getTaskStatusBoard(
@PathVariable("projectId") Long projectId,
@Valid ProjectTaskAggregateStatusBoardReqVO reqVO) {
return success(projectTaskAggregateService.getAggregateTaskStatusBoard(projectId, reqVO));
}
@GetMapping("/board-page")
@Operation(summary = "获取项目级跨执行任务看板分页")
public CommonResult<ProjectTaskBoardPageRespVO> getTaskBoardPage(
@PathVariable("projectId") Long projectId,
@Valid ProjectTaskAggregateBoardPageReqVO reqVO) {
return success(projectTaskAggregateService.getAggregateTaskBoardPage(projectId, reqVO));
}
@GetMapping("/summary")
@Operation(summary = "获取项目任务今日小条(支持 scope=mine|all)")
public CommonResult<ProjectTaskSummaryRespVO> getTaskSummary(
@PathVariable("projectId") Long projectId,
@Valid ProjectTaskSummaryReqVO reqVO) {
return success(projectTaskAggregateService.getAggregateTaskSummary(projectId, reqVO));
}
}

View File

@@ -0,0 +1,51 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 项目级跨执行任务看板分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectTaskAggregateBoardPageReqVO extends PageParam {
@Schema(description = "任务名称模糊匹配关键字")
private String keyword;
@Schema(description = "限定执行 id 列表;不传 = 项目内全部执行")
private List<Long> executionIds;
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
private List<String> executionStatusCodes;
@Schema(description = "我参与语义;与 ownerId 二选一")
private Long involveUserId;
@Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一")
private Long ownerId;
@Schema(description = "限定状态列(板上显示哪些列);空 / 不传 = 字典全状态")
private List<String> statusCodes;
@Schema(description = "优先级")
@Size(max = 8)
private String priority;
@Schema(description = "父任务 id")
private Long parentTaskId;
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
private String dueRange;
@Schema(description = "更新时间范围")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] updateTime;
}

View File

@@ -0,0 +1,57 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 项目级跨执行任务分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectTaskAggregatePageReqVO extends PageParam {
@Schema(description = "任务名称模糊匹配关键字")
private String keyword;
@Schema(description = "限定执行 id 列表;不传 = 项目内全部执行")
private List<Long> executionIds;
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
private List<String> executionStatusCodes;
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一")
private Long involveUserId;
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一")
private Long ownerId;
@Schema(description = "状态码多选;空 / 不传 = 全部")
private List<String> statusCodes;
@Schema(description = "优先级字典 value (0~3)")
@Size(max = 8)
private String priority;
@Schema(description = "父任务 id(限定到某父任务下)")
private Long parentTaskId;
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
private String dueRange;
@Schema(description = "更新时间范围(2 长度数组)")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] updateTime;
@Schema(description = "排序字段:plannedEndDate / priority / updateTime / createTime(默认 plannedEndDate)")
private String sortBy;
@Schema(description = "排序方向:asc / desc(默认 asc)")
private String sortOrder;
}

View File

@@ -0,0 +1,45 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 项目级跨执行任务状态看板 Request VO(入参同 page 去掉分页 / statusCodes / sort)")
@Data
public class ProjectTaskAggregateStatusBoardReqVO {
@Schema(description = "任务名称模糊匹配关键字")
private String keyword;
@Schema(description = "限定执行 id 列表;不传 = 项目内全部执行")
private List<Long> executionIds;
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
private List<String> executionStatusCodes;
@Schema(description = "我参与语义;与 ownerId 二选一")
private Long involveUserId;
@Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一")
private Long ownerId;
@Schema(description = "优先级")
@Size(max = 8)
private String priority;
@Schema(description = "父任务 id")
private Long parentTaskId;
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
private String dueRange;
@Schema(description = "更新时间范围")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] updateTime;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 项目任务今日小条 Request VO")
@Data
public class ProjectTaskSummaryReqVO {
/**
* 数字汇总作用域。
* <ul>
* <li>{@code mine}(默认):统计当前登录人 owner 或活跃协办的任务</li>
* <li>{@code all}:统计项目内全部任务,要求 {@code project:task:list-all} 权限码</li>
* </ul>
*/
@Schema(description = "作用域:mine(默认) / all", example = "mine")
private String scope;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
@Schema(description = "管理后台 - 项目任务今日小条 Response VO")
@Data
public class ProjectTaskSummaryRespVO {
@Schema(description = "逾期任务数:计划完成日 < 今天,且任务状态非终态")
private Long overdue;
@Schema(description = "今日截止任务数:计划完成日 = 今天,且任务状态非终态")
private Long dueToday;
@Schema(description = "本周到期任务数:计划完成日 ∈ [本周一, 本周日],且任务状态非终态(与 chip thisWeek 过滤同口径)")
private Long dueThisWeek;
@Schema(description = "本周已完成任务数:actualEndDate ∈ [本周一, 今天],且任务状态 = completed")
private Long doneThisWeek;
@Schema(description = "服务器当日(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-22")
private LocalDate today;
@Schema(description = "服务器本周一(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-18")
private LocalDate weekStart;
@Schema(description = "服务器本周日(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-24")
private LocalDate weekEnd;
}

View File

@@ -0,0 +1,36 @@
package com.njcn.rdms.module.project.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 任务到期范围快速筛选枚举(用于跨执行任务查询 chip)。
*
* <p>含义:</p>
* <ul>
* <li>{@link #OVERDUE} 计划完成日 &lt; 今天,且任务状态非终态</li>
* <li>{@link #TODAY} 计划完成日 = 今天</li>
* <li>{@link #THIS_WEEK} 计划完成日 在本周(周一~周日,Asia/Shanghai)</li>
* </ul>
*
* <p>非终态的判定走 {@code ObjectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled('task')}
* 动态查询,不在此枚举里硬编码状态字面值。</p>
*/
@Getter
@AllArgsConstructor
public enum DueRangeEnum {
OVERDUE("overdue"),
TODAY("today"),
THIS_WEEK("thisWeek");
private final String value;
public static DueRangeEnum of(String value) {
if (value == null) return null;
for (DueRangeEnum e : values()) {
if (e.value.equals(value)) return e;
}
return null;
}
}

View File

@@ -0,0 +1,30 @@
package com.njcn.rdms.module.project.service.project.task;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateBoardPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryRespVO;
/**
* 项目级跨执行任务查询 Service与执行级 {@link ProjectTaskService} 互补)。
* 服务于"我的任务"、"项目全部任务"两种跨执行视角。
*/
public interface ProjectTaskAggregateService {
PageResult<ProjectTaskRespVO> getAggregateTaskPage(Long projectId, ProjectTaskAggregatePageReqVO reqVO);
ProjectTaskStatusBoardRespVO getAggregateTaskStatusBoard(Long projectId, ProjectTaskAggregateStatusBoardReqVO reqVO);
ProjectTaskBoardPageRespVO getAggregateTaskBoardPage(Long projectId, ProjectTaskAggregateBoardPageReqVO reqVO);
/**
* scope=mine默认: 当前用户 owner 或活跃协办;
* scope=all: 全项目任务,要求 project:task:list-all 权限码,否则抛 PROJECT_OBJECT_PERMISSION_DENIED。
*/
ProjectTaskSummaryRespVO getAggregateTaskSummary(Long projectId, ProjectTaskSummaryReqVO reqVO);
}

View File

@@ -0,0 +1,301 @@
package com.njcn.rdms.module.project.service.project.task;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateBoardPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryRespVO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.framework.security.service.ProjectObjectPermissionService;
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.TemporalAdjusters;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateService {
private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Shanghai");
@Resource
private ProjectTaskMapper projectTaskMapper;
@Resource
private ProjectTaskService projectTaskService;
@Resource
private ObjectStatusModelMapper objectStatusModelMapper;
@Resource
private VisibilityScopeResolver visibilityScopeResolver;
@Resource
private ProjectObjectPermissionService projectObjectPermissionService;
// ========= 公共 helper =========
private LocalDate today() {
return LocalDate.now(SERVER_ZONE);
}
private LocalDate weekStart(LocalDate today) {
return today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
}
private LocalDate weekEnd(LocalDate today) {
return today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
}
private List<String> loadTerminalStatusCodes() {
return objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
}
/**
* 判定"项目全部"语义:既无 involveUserId 又无 ownerId。
*/
private boolean isAggregateAllScope(Long involveUserId, Long ownerId) {
return involveUserId == null && ownerId == null;
}
/**
* page / board-page / status-board 的 scope 解析(读路径"宽容降级"
* 入参组合 = allScopeIntent=true + 有 list-all → seesAll否则 → resolveForProject 过滤(不抛 403
*/
private VisibilityScope resolveScopeForRead(Long projectId, boolean allScopeIntent) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (allScopeIntent) {
boolean hasListAll = projectObjectPermissionService.hasPermission(projectId, ProjectTaskConstants.PERMISSION_LIST_ALL);
if (hasListAll) {
return VisibilityScope.all();
}
}
return visibilityScopeResolver.resolveForProject(projectId, userId);
}
// ========= page =========
@Override
public PageResult<ProjectTaskRespVO> getAggregateTaskPage(Long projectId, ProjectTaskAggregatePageReqVO reqVO) {
// 空数组语义短路:前端明确"按 0 个执行状态过滤" → 返空集合,不让 MyBatis 退化成"不过滤"
if (reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) {
return PageResult.empty();
}
boolean allScope = isAggregateAllScope(reqVO.getInvolveUserId(), reqVO.getOwnerId());
VisibilityScope scope = resolveScopeForRead(projectId, allScope);
LocalDate today = today();
LocalDate weekStart = weekStart(today);
LocalDate weekEnd = weekEnd(today);
List<String> terminalStatusCodes = loadTerminalStatusCodes();
Page<ProjectTaskDO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
IPage<ProjectTaskDO> ipage = projectTaskMapper.selectAggregatePageByProjectId(
projectId, scope.seesAll(), scope.taskIds(), reqVO, terminalStatusCodes, weekStart, weekEnd, today, page);
PageResult<ProjectTaskDO> doPage = new PageResult<>(ipage.getRecords(), ipage.getTotal());
return projectTaskService.assembleTaskRespVOPageCrossExecution(projectId, doPage);
}
// ========= status-board =========
@Override
public ProjectTaskStatusBoardRespVO getAggregateTaskStatusBoard(Long projectId, ProjectTaskAggregateStatusBoardReqVO reqVO) {
// 空数组语义短路:跳过 SQL,但保留状态列骨架(前端看板依赖全列表渲染),count 全部 0
if (reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) {
List<ObjectStatusModelDO> emptyStatusModels =
objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
return buildStatusBoardResponse(emptyStatusModels, Collections.emptyMap());
}
boolean allScope = isAggregateAllScope(reqVO.getInvolveUserId(), reqVO.getOwnerId());
VisibilityScope scope = resolveScopeForRead(projectId, allScope);
LocalDate today = today();
LocalDate weekStart = weekStart(today);
LocalDate weekEnd = weekEnd(today);
List<String> terminalStatusCodes = loadTerminalStatusCodes();
List<ObjectStatusModelDO> statusModels =
objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
List<ProjectTaskMapper.StatusCountRow> rows = projectTaskMapper.selectAggregateStatusCount(
projectId, scope.seesAll(), scope.taskIds(), reqVO, terminalStatusCodes, weekStart, weekEnd, today);
Map<String, Long> countMap = rows.stream()
.collect(Collectors.toMap(
ProjectTaskMapper.StatusCountRow::getStatusCode,
ProjectTaskMapper.StatusCountRow::getCount));
return buildStatusBoardResponse(statusModels, countMap);
}
/**
* 按状态模型全量列出count=0 的状态也输出(前端看板始终显示所有列)。
*/
private ProjectTaskStatusBoardRespVO buildStatusBoardResponse(
List<ObjectStatusModelDO> statusModels, Map<String, Long> countMap) {
List<ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO> items = statusModels.stream()
.map(sm -> {
ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO item =
new ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO();
item.setStatusCode(sm.getStatusCode());
item.setStatusName(sm.getStatusName());
item.setCount(countMap.getOrDefault(sm.getStatusCode(), 0L));
item.setSort(sm.getSort());
item.setTerminal(sm.getTerminalFlag());
return item;
})
.collect(Collectors.toList());
long total = items.stream().mapToLong(i -> i.getCount() == null ? 0L : i.getCount()).sum();
ProjectTaskStatusBoardRespVO resp = new ProjectTaskStatusBoardRespVO();
resp.setTotal(total);
resp.setItems(items);
return resp;
}
// ========= board-page =========
@Override
public ProjectTaskBoardPageRespVO getAggregateTaskBoardPage(Long projectId, ProjectTaskAggregateBoardPageReqVO reqVO) {
// 空数组语义短路:跳过 SQL,保留按 statusCodes 过滤后的列骨架,每列 list=[] / total=0
boolean emptyExecStatusCodes = reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty();
List<ObjectStatusModelDO> allStatusModels =
objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
List<ObjectStatusModelDO> targetStatusModels;
if (reqVO.getStatusCodes() == null || reqVO.getStatusCodes().isEmpty()) {
targetStatusModels = allStatusModels;
} else {
Set<String> filter = new HashSet<>(reqVO.getStatusCodes());
targetStatusModels = allStatusModels.stream()
.filter(sm -> filter.contains(sm.getStatusCode()))
.collect(Collectors.toList());
}
if (emptyExecStatusCodes) {
List<ProjectTaskBoardPageRespVO.ColumnItemVO> emptyItems = targetStatusModels.stream()
.map(sm -> buildColumnItemVO(sm, PageResult.empty()))
.collect(Collectors.toList());
ProjectTaskBoardPageRespVO emptyResp = new ProjectTaskBoardPageRespVO();
emptyResp.setItems(emptyItems);
return emptyResp;
}
boolean allScope = isAggregateAllScope(reqVO.getInvolveUserId(), reqVO.getOwnerId());
VisibilityScope scope = resolveScopeForRead(projectId, allScope);
LocalDate today = today();
LocalDate weekStart = weekStart(today);
LocalDate weekEnd = weekEnd(today);
List<String> terminalStatusCodes = loadTerminalStatusCodes();
List<ProjectTaskBoardPageRespVO.ColumnItemVO> items = targetStatusModels.stream()
.map(sm -> buildAggregateBoardColumn(projectId, scope, reqVO, sm,
terminalStatusCodes, today, weekStart, weekEnd))
.collect(Collectors.toList());
ProjectTaskBoardPageRespVO resp = new ProjectTaskBoardPageRespVO();
resp.setItems(items);
return resp;
}
private ProjectTaskBoardPageRespVO.ColumnItemVO buildAggregateBoardColumn(
Long projectId, VisibilityScope scope,
ProjectTaskAggregateBoardPageReqVO reqVO, ObjectStatusModelDO sm,
List<String> terminalStatusCodes, LocalDate today, LocalDate weekStart, LocalDate weekEnd) {
// 复用 selectAggregatePageByProjectId构造单列过滤的 innerReq
ProjectTaskAggregatePageReqVO innerReq = new ProjectTaskAggregatePageReqVO();
innerReq.setPageNo(reqVO.getPageNo());
innerReq.setPageSize(reqVO.getPageSize());
innerReq.setKeyword(reqVO.getKeyword());
innerReq.setExecutionIds(reqVO.getExecutionIds());
innerReq.setExecutionStatusCodes(reqVO.getExecutionStatusCodes());
innerReq.setInvolveUserId(reqVO.getInvolveUserId());
innerReq.setOwnerId(reqVO.getOwnerId());
innerReq.setStatusCodes(Collections.singletonList(sm.getStatusCode()));
innerReq.setPriority(reqVO.getPriority());
innerReq.setParentTaskId(reqVO.getParentTaskId());
innerReq.setDueRange(reqVO.getDueRange());
innerReq.setUpdateTime(reqVO.getUpdateTime());
// board-page 不传 sortBy / sortOrder用 page 查询的默认排序plannedEndDate ASC
Page<ProjectTaskDO> page = new Page<>(innerReq.getPageNo(), innerReq.getPageSize());
IPage<ProjectTaskDO> ipage = projectTaskMapper.selectAggregatePageByProjectId(
projectId, scope.seesAll(), scope.taskIds(), innerReq, terminalStatusCodes, weekStart, weekEnd, today, page);
PageResult<ProjectTaskDO> doPage = new PageResult<>(ipage.getRecords(), ipage.getTotal());
PageResult<ProjectTaskRespVO> voPage =
projectTaskService.assembleTaskRespVOPageCrossExecution(projectId, doPage);
return buildColumnItemVO(sm, voPage);
}
private ProjectTaskBoardPageRespVO.ColumnItemVO buildColumnItemVO(
ObjectStatusModelDO sm, PageResult<ProjectTaskRespVO> voPage) {
ProjectTaskBoardPageRespVO.ColumnItemVO col = new ProjectTaskBoardPageRespVO.ColumnItemVO();
col.setStatusCode(sm.getStatusCode());
col.setStatusName(sm.getStatusName());
col.setSort(sm.getSort());
col.setTerminal(sm.getTerminalFlag());
col.setList(voPage.getList());
col.setTotal(voPage.getTotal());
return col;
}
// ========= summary =========
@Override
public ProjectTaskSummaryRespVO getAggregateTaskSummary(Long projectId, ProjectTaskSummaryReqVO reqVO) {
boolean isAll = "all".equalsIgnoreCase(reqVO.getScope());
if (isAll) {
// scope=all 强校验 list-all无权抛 403与 page/board-page/status-board 的"宽容降级"不同)
projectObjectPermissionService.checkPermission(projectId, ProjectTaskConstants.PERMISSION_LIST_ALL, false);
}
LocalDate today = today();
LocalDate weekStart = weekStart(today);
LocalDate weekEnd = weekEnd(today);
List<String> terminalStatusCodes = loadTerminalStatusCodes();
Long userId = SecurityFrameworkUtils.getLoginUserId();
Long involveUserId = isAll ? null : userId;
VisibilityScope scope = isAll
? VisibilityScope.all()
: visibilityScopeResolver.resolveForProject(projectId, userId);
Map<String, Long> counts = projectTaskMapper.selectAggregateSummaryCounts(
projectId, scope.seesAll(), scope.taskIds(), involveUserId,
terminalStatusCodes, ProjectTaskConstants.STATUS_COMPLETED,
today, weekStart, weekEnd);
ProjectTaskSummaryRespVO resp = new ProjectTaskSummaryRespVO();
resp.setOverdue(zeroIfNull(counts.get("overdue")));
resp.setDueToday(zeroIfNull(counts.get("dueToday")));
resp.setDueThisWeek(zeroIfNull(counts.get("dueThisWeek")));
resp.setDoneThisWeek(zeroIfNull(counts.get("doneThisWeek")));
resp.setToday(today);
resp.setWeekStart(weekStart);
resp.setWeekEnd(weekEnd);
return resp;
}
private Long zeroIfNull(Long v) {
return v == null ? 0L : v;
}
}