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:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.njcn.rdms.module.project.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 任务到期范围快速筛选枚举(用于跨执行任务查询 chip)。
|
||||
*
|
||||
* <p>含义:</p>
|
||||
* <ul>
|
||||
* <li>{@link #OVERDUE} 计划完成日 < 今天,且任务状态非终态</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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user