From df13a90107435521254501f8b12a01003b4ace2f Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Sat, 23 May 2026 14:18:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(project):=20=E6=B7=BB=E5=8A=A0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E4=BB=BB=E5=8A=A1=E8=B7=A8=E6=89=A7=E8=A1=8C=E8=81=9A?= =?UTF-8?q?=E5=90=88=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ObjectPermissionService.hasPermission 非抛模式权限检查方法 - 实现 ProjectObjectPermissionService.hasPermission 权限验证逻辑 - 为 ProductObjectPermissionService 预留空实现并添加日志警告 - 在 ProjectExecutionController 中支持负数 pageSize 查询全部功能 - 添加 ProjectTaskConstants.PERMISSION_LIST_ALL 权限码常量定义 - 扩展 ProjectTaskMapper 支持跨执行聚合分页、状态统计和摘要查询 - 更新 ProjectTaskRespVO 包含执行名称和状态码字段 - 实现 ProjectTaskService.assembleTaskRespVOPageCrossExecution 跨执行装配方法 - 优化任务服务中的执行信息批量回填和生命周期应用逻辑 - 统一使用服务器时区 Asia/Shanghai 处理日期时间操作 - 为 .claude 设置添加新的代码搜索和分析命令 --- .../task/ProjectTaskAggregateController.java | 66 ++++ .../ProjectTaskAggregateBoardPageReqVO.java | 51 +++ .../ProjectTaskAggregatePageReqVO.java | 57 ++++ .../ProjectTaskAggregateStatusBoardReqVO.java | 45 +++ .../vo/aggregate/ProjectTaskSummaryReqVO.java | 19 ++ .../aggregate/ProjectTaskSummaryRespVO.java | 32 ++ .../module/project/enums/DueRangeEnum.java | 36 +++ .../task/ProjectTaskAggregateService.java | 30 ++ .../task/ProjectTaskAggregateServiceImpl.java | 301 ++++++++++++++++++ 9 files changed, 637 insertions(+) create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/ProjectTaskAggregateController.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregateBoardPageReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregatePageReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregateStatusBoardReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskSummaryReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskSummaryRespVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/enums/DueRangeEnum.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateServiceImpl.java diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/ProjectTaskAggregateController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/ProjectTaskAggregateController.java new file mode 100644 index 0000000..0281af3 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/ProjectTaskAggregateController.java @@ -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> getTaskPage( + @PathVariable("projectId") Long projectId, + @Valid ProjectTaskAggregatePageReqVO reqVO) { + return success(projectTaskAggregateService.getAggregateTaskPage(projectId, reqVO)); + } + + @GetMapping("/status-board") + @Operation(summary = "获取项目级跨执行任务状态看板") + public CommonResult getTaskStatusBoard( + @PathVariable("projectId") Long projectId, + @Valid ProjectTaskAggregateStatusBoardReqVO reqVO) { + return success(projectTaskAggregateService.getAggregateTaskStatusBoard(projectId, reqVO)); + } + + @GetMapping("/board-page") + @Operation(summary = "获取项目级跨执行任务看板分页") + public CommonResult getTaskBoardPage( + @PathVariable("projectId") Long projectId, + @Valid ProjectTaskAggregateBoardPageReqVO reqVO) { + return success(projectTaskAggregateService.getAggregateTaskBoardPage(projectId, reqVO)); + } + + @GetMapping("/summary") + @Operation(summary = "获取项目任务今日小条(支持 scope=mine|all)") + public CommonResult getTaskSummary( + @PathVariable("projectId") Long projectId, + @Valid ProjectTaskSummaryReqVO reqVO) { + return success(projectTaskAggregateService.getAggregateTaskSummary(projectId, reqVO)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregateBoardPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregateBoardPageReqVO.java new file mode 100644 index 0000000..d6d1771 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregateBoardPageReqVO.java @@ -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 executionIds; + + @Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤") + private List executionStatusCodes; + + @Schema(description = "我参与语义;与 ownerId 二选一") + private Long involveUserId; + + @Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一") + private Long ownerId; + + @Schema(description = "限定状态列(板上显示哪些列);空 / 不传 = 字典全状态") + private List 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; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregatePageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregatePageReqVO.java new file mode 100644 index 0000000..192b363 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregatePageReqVO.java @@ -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 executionIds; + + @Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤") + private List executionStatusCodes; + + @Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一") + private Long involveUserId; + + @Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一") + private Long ownerId; + + @Schema(description = "状态码多选;空 / 不传 = 全部") + private List 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; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregateStatusBoardReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregateStatusBoardReqVO.java new file mode 100644 index 0000000..3c91710 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskAggregateStatusBoardReqVO.java @@ -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 executionIds; + + @Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤") + private List 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; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskSummaryReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskSummaryReqVO.java new file mode 100644 index 0000000..50fc8f5 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskSummaryReqVO.java @@ -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 { + + /** + * 数字汇总作用域。 + *
    + *
  • {@code mine}(默认):统计当前登录人 owner 或活跃协办的任务
  • + *
  • {@code all}:统计项目内全部任务,要求 {@code project:task:list-all} 权限码
  • + *
+ */ + @Schema(description = "作用域:mine(默认) / all", example = "mine") + private String scope; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskSummaryRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskSummaryRespVO.java new file mode 100644 index 0000000..ab3d83e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/aggregate/ProjectTaskSummaryRespVO.java @@ -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; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/enums/DueRangeEnum.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/enums/DueRangeEnum.java new file mode 100644 index 0000000..3b8cd49 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/enums/DueRangeEnum.java @@ -0,0 +1,36 @@ +package com.njcn.rdms.module.project.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 任务到期范围快速筛选枚举(用于跨执行任务查询 chip)。 + * + *

含义:

+ *
    + *
  • {@link #OVERDUE} 计划完成日 < 今天,且任务状态非终态
  • + *
  • {@link #TODAY} 计划完成日 = 今天
  • + *
  • {@link #THIS_WEEK} 计划完成日 在本周(周一~周日,Asia/Shanghai)
  • + *
+ * + *

非终态的判定走 {@code ObjectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled('task')} + * 动态查询,不在此枚举里硬编码状态字面值。

+ */ +@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; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateService.java new file mode 100644 index 0000000..7c9e7bd --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateService.java @@ -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 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); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateServiceImpl.java new file mode 100644 index 0000000..64bf6e3 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskAggregateServiceImpl.java @@ -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 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 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 terminalStatusCodes = loadTerminalStatusCodes(); + + Page page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize()); + IPage ipage = projectTaskMapper.selectAggregatePageByProjectId( + projectId, scope.seesAll(), scope.taskIds(), reqVO, terminalStatusCodes, weekStart, weekEnd, today, page); + PageResult 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 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 terminalStatusCodes = loadTerminalStatusCodes(); + + List statusModels = + objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); + + List rows = projectTaskMapper.selectAggregateStatusCount( + projectId, scope.seesAll(), scope.taskIds(), reqVO, terminalStatusCodes, weekStart, weekEnd, today); + Map countMap = rows.stream() + .collect(Collectors.toMap( + ProjectTaskMapper.StatusCountRow::getStatusCode, + ProjectTaskMapper.StatusCountRow::getCount)); + + return buildStatusBoardResponse(statusModels, countMap); + } + + /** + * 按状态模型全量列出,count=0 的状态也输出(前端看板始终显示所有列)。 + */ + private ProjectTaskStatusBoardRespVO buildStatusBoardResponse( + List statusModels, Map countMap) { + List 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 allStatusModels = + objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); + List targetStatusModels; + if (reqVO.getStatusCodes() == null || reqVO.getStatusCodes().isEmpty()) { + targetStatusModels = allStatusModels; + } else { + Set filter = new HashSet<>(reqVO.getStatusCodes()); + targetStatusModels = allStatusModels.stream() + .filter(sm -> filter.contains(sm.getStatusCode())) + .collect(Collectors.toList()); + } + + if (emptyExecStatusCodes) { + List 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 terminalStatusCodes = loadTerminalStatusCodes(); + + List 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 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 page = new Page<>(innerReq.getPageNo(), innerReq.getPageSize()); + IPage ipage = projectTaskMapper.selectAggregatePageByProjectId( + projectId, scope.seesAll(), scope.taskIds(), innerReq, terminalStatusCodes, weekStart, weekEnd, today, page); + PageResult doPage = new PageResult<>(ipage.getRecords(), ipage.getTotal()); + PageResult voPage = + projectTaskService.assembleTaskRespVOPageCrossExecution(projectId, doPage); + + return buildColumnItemVO(sm, voPage); + } + + private ProjectTaskBoardPageRespVO.ColumnItemVO buildColumnItemVO( + ObjectStatusModelDO sm, PageResult 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 terminalStatusCodes = loadTerminalStatusCodes(); + + Long userId = SecurityFrameworkUtils.getLoginUserId(); + Long involveUserId = isAll ? null : userId; + VisibilityScope scope = isAll + ? VisibilityScope.all() + : visibilityScopeResolver.resolveForProject(projectId, userId); + + Map 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; + } +}