Compare commits
2 Commits
c9549bed46
...
df13a90107
| Author | SHA1 | Date | |
|---|---|---|---|
| df13a90107 | |||
| 8a36b49128 |
@@ -105,7 +105,10 @@
|
|||||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; $out = & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests 2>&1; $code = $LASTEXITCODE; $out | Select-Object -Last 15; Write-Output \"EXIT=$code\")",
|
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; $out = & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests 2>&1; $code = $LASTEXITCODE; $out | Select-Object -Last 15; Write-Output \"EXIT=$code\")",
|
||||||
"Bash(set \"JAVA_HOME=C:\\\\Program Files\\\\Java\\\\jdk-17\")",
|
"Bash(set \"JAVA_HOME=C:\\\\Program Files\\\\Java\\\\jdk-17\")",
|
||||||
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
|
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
|
||||||
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)"
|
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)",
|
||||||
|
"Bash(grep -E \"\\\\.\\(sql|java|md\\)$\")",
|
||||||
|
"Bash(xargs grep -l \"INSERT INTO.*system_menu\")",
|
||||||
|
"Bash(Get-ChildItem *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ public final class ProjectTaskConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String PERMISSION_DELETE = "project:task:delete";
|
public static final String PERMISSION_DELETE = "project:task:delete";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目任务"查看全部"权限码(对象域,object_type='project')。
|
||||||
|
*
|
||||||
|
* 用于跨执行视角下"项目全部任务"语义的接口/视图(page / status-board /
|
||||||
|
* board-page / summary 的 scope=all 分支)。无此权限码时,只能看到自己作为
|
||||||
|
* owner 或活跃协办的任务(走 VisibilityScopeResolver.resolveForProject 过滤)。
|
||||||
|
*
|
||||||
|
* 种子绑定:默认绑给项目负责人 + 项目创建人;普通成员、执行负责人默认不绑。
|
||||||
|
* 实际项目中由运维在角色管理界面调整。
|
||||||
|
*/
|
||||||
|
public static final String PERMISSION_LIST_ALL = "project:task:list-all";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除确认口令合法值集合;兼容大写英文 "DELETE" 与中文 "删除",前端可纯中文文案。
|
* 删除确认口令合法值集合;兼容大写英文 "DELETE" 与中文 "删除",前端可纯中文文案。
|
||||||
* 校验时精确匹配(trim 后比对)。
|
* 校验时精确匹配(trim 后比对)。
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
||||||
|
|
||||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||||
@@ -65,6 +66,11 @@ public class ProjectExecutionController {
|
|||||||
@Operation(summary = "获取执行分页")
|
@Operation(summary = "获取执行分页")
|
||||||
public CommonResult<PageResult<ProjectExecutionRespVO>> getExecutionPage(@PathVariable("projectId") Long projectId,
|
public CommonResult<PageResult<ProjectExecutionRespVO>> getExecutionPage(@PathVariable("projectId") Long projectId,
|
||||||
@Valid ProjectExecutionPageReqVO reqVO) {
|
@Valid ProjectExecutionPageReqVO reqVO) {
|
||||||
|
// 前端用负数 pageSize 表示"查询全部",统一归一为框架约定的 PAGE_SIZE_NONE(-1),
|
||||||
|
// 走 BaseMapperX.selectPage 的不分页短路;@Max(200) 仅拦上界,负数不会被 validator 卡。
|
||||||
|
if (reqVO.getPageSize() != null && reqVO.getPageSize() < 0) {
|
||||||
|
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||||
|
}
|
||||||
return success(projectExecutionService.getExecutionRespVOPage(projectId, reqVO));
|
return success(projectExecutionService.getExecutionRespVOPage(projectId, reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ public class ProjectTaskRespVO {
|
|||||||
private Long projectId;
|
private Long projectId;
|
||||||
@Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
@Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
||||||
private Long executionId;
|
private Long executionId;
|
||||||
|
@Schema(description = "所属执行名称", example = "迭代 V1.0")
|
||||||
|
private String executionName;
|
||||||
|
@Schema(description = "所属执行状态码")
|
||||||
|
private String executionStatusCode;
|
||||||
@Schema(description = "所属执行关联的项目需求编号(service 层批量回填)")
|
@Schema(description = "所属执行关联的项目需求编号(service 层批量回填)")
|
||||||
private Long projectRequirementId;
|
private Long projectRequirementId;
|
||||||
@Schema(description = "项目需求名称(service 层批量回填,避免 N+1)", example = "前端联调-需求A")
|
@Schema(description = "项目需求名称(service 层批量回填,避免 N+1)", example = "前端联调-需求A")
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
package com.njcn.rdms.module.project.dal.mysql.project.task;
|
package com.njcn.rdms.module.project.dal.mysql.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.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
|
||||||
|
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.dal.dataobject.project.task.ProjectTaskDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
||||||
|
import lombok.Data;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
@@ -379,4 +384,276 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
.eq(ProjectTaskDO::getExecutionId, executionId)));
|
.eq(ProjectTaskDO::getExecutionId, executionId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== 项目级跨执行聚合查询 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目级跨执行任务分页查询。
|
||||||
|
*
|
||||||
|
* 语义:
|
||||||
|
* - scope.seesAll() = true → 不附加 taskIds 过滤
|
||||||
|
* - scope.seesAll() = false → 附加 t.id IN (scope.taskIds()) 短路过滤
|
||||||
|
* - involveUserId 不为 null → 附加 (t.owner_id = ? OR exists active assignee user_id = ?)
|
||||||
|
* - statusCodes 非空 → t.status_code IN (...)
|
||||||
|
* - dueRange='overdue' 且 terminalStatusCodes 非空 → 排除终态
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT t.*
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="!seesAll and visibleTaskIds != null and !visibleTaskIds.isEmpty()">
|
||||||
|
AND t.id IN
|
||||||
|
<foreach collection="visibleTaskIds" item="tid" open="(" separator="," close=")">#{tid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="!seesAll and (visibleTaskIds == null or visibleTaskIds.isEmpty())">
|
||||||
|
AND 1=0
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionIds != null and !reqVO.executionIds.isEmpty()">
|
||||||
|
AND t.execution_id IN
|
||||||
|
<foreach collection="reqVO.executionIds" item="eid" open="(" separator="," close=")">#{eid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionStatusCodes != null and !reqVO.executionStatusCodes.isEmpty()">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND e.status_code IN
|
||||||
|
<foreach collection="reqVO.executionStatusCodes" item="esc" open="(" separator="," close=")">#{esc}</foreach>
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.statusCodes != null and !reqVO.statusCodes.isEmpty()">
|
||||||
|
AND t.status_code IN
|
||||||
|
<foreach collection="reqVO.statusCodes" item="sc" open="(" separator="," close=")">#{sc}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
<choose>
|
||||||
|
<when test="reqVO.sortBy == 'priority'">ORDER BY t.priority</when>
|
||||||
|
<when test="reqVO.sortBy == 'updateTime'">ORDER BY t.update_time</when>
|
||||||
|
<when test="reqVO.sortBy == 'createTime'">ORDER BY t.create_time</when>
|
||||||
|
<otherwise>ORDER BY t.planned_end_date</otherwise>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<when test="reqVO.sortOrder == 'desc'">DESC</when>
|
||||||
|
<otherwise>ASC</otherwise>
|
||||||
|
</choose>
|
||||||
|
, t.id DESC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
IPage<ProjectTaskDO> selectAggregatePageByProjectId(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("seesAll") boolean seesAll,
|
||||||
|
@Param("visibleTaskIds") java.util.Collection<Long> visibleTaskIds,
|
||||||
|
@Param("reqVO") ProjectTaskAggregatePageReqVO reqVO,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
Page<ProjectTaskDO> page);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目级跨执行任务按状态分组计数(status-board)。
|
||||||
|
* 入参同 page 但去除分页 / sort / statusCodes 筛选。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT t.status_code AS statusCode, COUNT(*) AS count
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="!seesAll and visibleTaskIds != null and !visibleTaskIds.isEmpty()">
|
||||||
|
AND t.id IN
|
||||||
|
<foreach collection="visibleTaskIds" item="tid" open="(" separator="," close=")">#{tid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="!seesAll and (visibleTaskIds == null or visibleTaskIds.isEmpty())">
|
||||||
|
AND 1=0
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionIds != null and !reqVO.executionIds.isEmpty()">
|
||||||
|
AND t.execution_id IN
|
||||||
|
<foreach collection="reqVO.executionIds" item="eid" open="(" separator="," close=")">#{eid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionStatusCodes != null and !reqVO.executionStatusCodes.isEmpty()">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND e.status_code IN
|
||||||
|
<foreach collection="reqVO.executionStatusCodes" item="esc" open="(" separator="," close=")">#{esc}</foreach>
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
GROUP BY t.status_code
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<StatusCountRow> selectAggregateStatusCount(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("seesAll") boolean seesAll,
|
||||||
|
@Param("visibleTaskIds") java.util.Collection<Long> visibleTaskIds,
|
||||||
|
@Param("reqVO") ProjectTaskAggregateStatusBoardReqVO reqVO,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd,
|
||||||
|
@Param("today") LocalDate today);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* summary 的 4 个数字一次查出来,避免 4 次扫表。
|
||||||
|
*
|
||||||
|
* 返回 Map 结构:
|
||||||
|
* overdue → Long
|
||||||
|
* dueToday → Long
|
||||||
|
* dueThisWeek → Long
|
||||||
|
* doneThisWeek → Long
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS overdue,
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date = #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS dueToday,
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS dueThisWeek,
|
||||||
|
CAST(SUM(CASE WHEN t.actual_end_date BETWEEN #{weekStart} AND #{today}
|
||||||
|
AND t.status_code = #{completedStatusCode}
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS doneThisWeek
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="!seesAll and visibleTaskIds != null and !visibleTaskIds.isEmpty()">
|
||||||
|
AND t.id IN
|
||||||
|
<foreach collection="visibleTaskIds" item="tid" open="(" separator="," close=")">#{tid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="!seesAll and (visibleTaskIds == null or visibleTaskIds.isEmpty())">
|
||||||
|
AND 1=0
|
||||||
|
</if>
|
||||||
|
<if test="involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Map<String, Long> selectAggregateSummaryCounts(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("seesAll") boolean seesAll,
|
||||||
|
@Param("visibleTaskIds") java.util.Collection<Long> visibleTaskIds,
|
||||||
|
@Param("involveUserId") Long involveUserId,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("completedStatusCode") String completedStatusCode,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* status-board 计数行 — 内嵌静态类,与 mapper 共享生命周期。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
class StatusCountRow {
|
||||||
|
private String statusCode;
|
||||||
|
private Long count;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,4 +19,16 @@ public interface ObjectPermissionService {
|
|||||||
*/
|
*/
|
||||||
void checkPermission(Long objectId, String permission, boolean memberOnly);
|
void checkPermission(Long objectId, String permission, boolean memberOnly);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前登录用户是否具备指定对象上的指定权限码(非抛模式)。
|
||||||
|
*
|
||||||
|
* 与 {@link #checkPermission(Long, String, boolean)} 区别:
|
||||||
|
* 本方法不抛异常,纯返回 boolean,用于"无权限就走降级路径"而非"无权限就 403"的场景。
|
||||||
|
*
|
||||||
|
* @param objectId 对象 ID(如 projectId)
|
||||||
|
* @param permission 权限码,如 {@code project:task:list-all}
|
||||||
|
* @return true=具备,false=不具备
|
||||||
|
*/
|
||||||
|
boolean hasPermission(Long objectId, String permission);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
|||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
/**
|
/**
|
||||||
* 产品对象权限服务。
|
* 产品对象权限服务。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class ProductObjectPermissionService implements ObjectPermissionService {
|
public class ProductObjectPermissionService implements ObjectPermissionService {
|
||||||
|
|
||||||
@@ -36,6 +38,15 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
|||||||
return ProductObjectConstants.OBJECT_TYPE;
|
return ProductObjectConstants.OBJECT_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(Long objectId, String permission) {
|
||||||
|
// 当前产品域无 hasPermission 非抛模式调用场景,预留空实现。
|
||||||
|
// 启用时参考 ProjectObjectPermissionService.hasPermission 同款实现。
|
||||||
|
log.warn("[ProductObjectPermissionService.hasPermission] 未实现,默认返回 false;objectId={}, permission={}",
|
||||||
|
objectId, permission);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkPermission(Long objectId, String permission, boolean memberOnly) {
|
public void checkPermission(Long objectId, String permission, boolean memberOnly) {
|
||||||
if (objectId == null) {
|
if (objectId == null) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.njcn.rdms.module.project.framework.security.service;
|
package com.njcn.rdms.module.project.framework.security.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
@@ -8,6 +9,7 @@ import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
|||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
/**
|
/**
|
||||||
* 项目对象权限服务。
|
* 项目对象权限服务。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class ProjectObjectPermissionService implements ObjectPermissionService {
|
public class ProjectObjectPermissionService implements ObjectPermissionService {
|
||||||
|
|
||||||
@@ -53,7 +56,7 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String normalizedPermission = normalizePermission(permission);
|
String normalizedPermission = normalizePermission(permission);
|
||||||
// 任一角色含该权限码即放行(等价于多角色 union;短路求值,权限码命中早 return)
|
// 任一角色含该权限码即放行(等价于多角色 union;短路求值)
|
||||||
boolean allowed = userRoles.stream()
|
boolean allowed = userRoles.stream()
|
||||||
.map(UserObjectRoleDO::getRoleId)
|
.map(UserObjectRoleDO::getRoleId)
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -63,6 +66,33 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(Long objectId, String permission) {
|
||||||
|
if (objectId == null || StrUtil.isBlank(permission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 先确认是对象成员,非成员直接返回 false
|
||||||
|
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||||
|
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, userId);
|
||||||
|
if (userRoles.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalizedPermission = permission.trim();
|
||||||
|
return userRoles.stream()
|
||||||
|
.map(UserObjectRoleDO::getRoleId)
|
||||||
|
.distinct()
|
||||||
|
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[hasPermission] objectId={}, permission={} 查询权限失败", objectId, permission, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Set<String> getRolePermissions(Long roleId) {
|
private Set<String> getRolePermissions(Long roleId) {
|
||||||
Set<String> permissions = objectPermissionApi
|
Set<String> permissions = objectPermissionApi
|
||||||
.getObjectRolePermissions(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
.getObjectRolePermissions(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,18 @@ public interface ProjectTaskService {
|
|||||||
PageResult<ProjectTaskRespVO> assembleTaskRespVOPage(Long projectId, Long executionId,
|
PageResult<ProjectTaskRespVO> assembleTaskRespVOPage(Long projectId, Long executionId,
|
||||||
PageResult<ProjectTaskDO> doPage);
|
PageResult<ProjectTaskDO> doPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨执行装配 ProjectTaskRespVO 分页结果。
|
||||||
|
*
|
||||||
|
* 与 {@link #assembleTaskRespVOPage(Long, Long, PageResult)} 区别:
|
||||||
|
* 本方法不绑单个 executionId,允许 page 内任务来自项目下任意多个执行;
|
||||||
|
* 装配时按 task.executionId 分组批量回填 executionName / executionStatusCode
|
||||||
|
* (走 enrichExecutionInfo helper)。
|
||||||
|
* <p><b>不填充字段</b>:executionOwnerId / projectRequirementName / projectRequirementStatusCode
|
||||||
|
* (跨多 execution 场景无法共享单 execution 上下文)。前端跨执行视图按需处理。</p>
|
||||||
|
*/
|
||||||
|
PageResult<ProjectTaskRespVO> assembleTaskRespVOPageCrossExecution(Long projectId, PageResult<ProjectTaskDO> doPage);
|
||||||
|
|
||||||
void changeTaskStatus(Long projectId, Long executionId, Long taskId, ProjectTaskStatusActionReqVO reqVO);
|
void changeTaskStatus(Long projectId, Long executionId, Long taskId, ProjectTaskStatusActionReqVO reqVO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.njcn.rdms.module.project.service.project.task;
|
package com.njcn.rdms.module.project.service.project.task;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||||
@@ -62,6 +63,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -85,6 +87,8 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ProjectTaskServiceImpl implements ProjectTaskService {
|
public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||||
|
|
||||||
|
private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ProjectMapper projectMapper;
|
private ProjectMapper projectMapper;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -462,6 +466,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
.loadActiveAssigneesGroupedByTaskId(List.of(task.getId())).getOrDefault(task.getId(), List.of())));
|
.loadActiveAssigneesGroupedByTaskId(List.of(task.getId())).getOrDefault(task.getId(), List.of())));
|
||||||
respVO.setTotalSpentHours(taskWorklogService.sumDurationByTaskId(task.getId()));
|
respVO.setTotalSpentHours(taskWorklogService.sumDurationByTaskId(task.getId()));
|
||||||
respVO.setExecutionOwnerId(execution.getOwnerId());
|
respVO.setExecutionOwnerId(execution.getOwnerId());
|
||||||
|
respVO.setExecutionName(execution.getExecutionName());
|
||||||
|
respVO.setExecutionStatusCode(execution.getStatusCode());
|
||||||
if (task.getParentTaskId() != null) {
|
if (task.getParentTaskId() != null) {
|
||||||
ProjectTaskDO parentTask = projectTaskMapper.selectById(task.getParentTaskId());
|
ProjectTaskDO parentTask = projectTaskMapper.selectById(task.getParentTaskId());
|
||||||
respVO.setParentTaskOwnerId(parentTask == null ? null : parentTask.getOwnerId());
|
respVO.setParentTaskOwnerId(parentTask == null ? null : parentTask.getOwnerId());
|
||||||
@@ -489,40 +495,15 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
if (list == null || list.isEmpty()) {
|
if (list == null || list.isEmpty()) {
|
||||||
return voPageResult;
|
return voPageResult;
|
||||||
}
|
}
|
||||||
// 批量装配 ownerNickname + assignees,统一收集所有需要的 userId 一次性查 nickname,避免 N+1
|
List<ProjectTaskDO> taskList = pageResult.getList();
|
||||||
Set<Long> taskIds = list.stream().map(ProjectTaskRespVO::getId)
|
|
||||||
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
Map<Long, List<TaskAssigneeDO>> assigneeMap = taskAssigneeService
|
|
||||||
.loadActiveAssigneesGroupedByTaskId(taskIds);
|
|
||||||
Map<Long, BigDecimal> spentHoursMap = taskWorklogService.sumDurationGroupedByTaskIds(taskIds);
|
|
||||||
Set<Long> userIdsToResolve = list.stream()
|
|
||||||
.map(ProjectTaskRespVO::getOwnerId)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
assigneeMap.values().forEach(items -> items.forEach(a -> userIdsToResolve.add(a.getUserId())));
|
|
||||||
Map<Long, String> nicknameMap = loadOwnerNicknameMap(userIdsToResolve);
|
|
||||||
// 批量查父任务 owner,避免按 list 循环 N+1
|
|
||||||
Set<Long> parentTaskIds = list.stream().map(ProjectTaskRespVO::getParentTaskId)
|
|
||||||
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
Map<Long, Long> parentTaskOwnerMap = parentTaskIds.isEmpty()
|
|
||||||
? Map.of()
|
|
||||||
: projectTaskMapper.selectBatchIds(parentTaskIds).stream()
|
|
||||||
.collect(Collectors.toMap(ProjectTaskDO::getId, ProjectTaskDO::getOwnerId));
|
|
||||||
// 执行 owner 单条查询,整页共享(URL 路径定 executionId)
|
// 执行 owner 单条查询,整页共享(URL 路径定 executionId)
|
||||||
ProjectExecutionDO execution = projectExecutionMapper.selectByProjectIdAndId(projectId, executionId);
|
ProjectExecutionDO execution = projectExecutionMapper.selectByProjectIdAndId(projectId, executionId);
|
||||||
Long executionOwnerId = execution == null ? null : execution.getOwnerId();
|
Long executionOwnerId = execution == null ? null : execution.getOwnerId();
|
||||||
fillProjectRequirementInfo(list, execution);
|
fillProjectRequirementInfo(list, execution);
|
||||||
|
// 批量回填 ownerNickname / assignees / totalSpentHours / parentTaskOwnerId(与 executionId 无关)
|
||||||
|
enrichTaskRelations(taskList, list);
|
||||||
list.forEach(vo -> {
|
list.forEach(vo -> {
|
||||||
vo.setOwnerNickname(nicknameMap.get(vo.getOwnerId()));
|
|
||||||
List<TaskAssigneeDO> activeList = assigneeMap.getOrDefault(vo.getId(), List.of());
|
|
||||||
vo.setAssignees(activeList.stream()
|
|
||||||
.map(a -> toAssigneeView(a, nicknameMap.get(a.getUserId())))
|
|
||||||
.collect(Collectors.toList()));
|
|
||||||
vo.setTotalSpentHours(spentHoursMap.getOrDefault(vo.getId(), BigDecimal.ZERO));
|
|
||||||
vo.setExecutionOwnerId(executionOwnerId);
|
vo.setExecutionOwnerId(executionOwnerId);
|
||||||
if (vo.getParentTaskId() != null) {
|
|
||||||
vo.setParentTaskOwnerId(parentTaskOwnerMap.get(vo.getParentTaskId()));
|
|
||||||
}
|
|
||||||
// 列表行 cancel/pause/resume/complete 按钮依赖 availableActions,与详情同款装配 lifecycle。
|
// 列表行 cancel/pause/resume/complete 按钮依赖 availableActions,与详情同款装配 lifecycle。
|
||||||
// 单行装配失败做兜底降级(status_model 缺失等脏数据),避免影响整页返回。
|
// 单行装配失败做兜底降级(status_model 缺失等脏数据),避免影响整页返回。
|
||||||
try {
|
try {
|
||||||
@@ -532,9 +513,112 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
vo.getId(), vo.getStatusCode(), e.getMessage());
|
vo.getId(), vo.getStatusCode(), e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
enrichExecutionInfo(voPageResult.getList());
|
||||||
return voPageResult;
|
return voPageResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量回填与 executionId 无关的任务关联字段:ownerNickname / assignees / totalSpentHours / parentTaskOwnerId。
|
||||||
|
* <p>由 {@link #assembleTaskRespVOPage} 与 {@link #assembleTaskRespVOPageCrossExecution} 共享,
|
||||||
|
* 抽取后两条装配路径保证字段口径一致,避免演进漂移。
|
||||||
|
*
|
||||||
|
* @param taskList 原始 DO 列表(用于 enrichTaskRelations 内部如有需要直接访问 DO 字段)
|
||||||
|
* @param voList 已 BeanUtils.toBean 转换好的 VO 列表,本方法在其上 set 字段
|
||||||
|
*/
|
||||||
|
private void enrichTaskRelations(List<ProjectTaskDO> taskList, List<ProjectTaskRespVO> voList) {
|
||||||
|
if (CollUtil.isEmpty(voList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 批量装配 ownerNickname + assignees,统一收集所有需要的 userId 一次性查 nickname,避免 N+1
|
||||||
|
Set<Long> taskIds = voList.stream().map(ProjectTaskRespVO::getId)
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
Map<Long, List<TaskAssigneeDO>> assigneeMap = taskAssigneeService
|
||||||
|
.loadActiveAssigneesGroupedByTaskId(taskIds);
|
||||||
|
Map<Long, BigDecimal> spentHoursMap = taskWorklogService.sumDurationGroupedByTaskIds(taskIds);
|
||||||
|
Set<Long> userIdsToResolve = voList.stream()
|
||||||
|
.map(ProjectTaskRespVO::getOwnerId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
assigneeMap.values().forEach(items -> items.forEach(a -> userIdsToResolve.add(a.getUserId())));
|
||||||
|
Map<Long, String> nicknameMap = loadOwnerNicknameMap(userIdsToResolve);
|
||||||
|
// 批量查父任务 owner,避免按 list 循环 N+1
|
||||||
|
Set<Long> parentTaskIds = voList.stream().map(ProjectTaskRespVO::getParentTaskId)
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
Map<Long, Long> parentTaskOwnerMap;
|
||||||
|
if (parentTaskIds.isEmpty()) {
|
||||||
|
parentTaskOwnerMap = Map.of();
|
||||||
|
} else {
|
||||||
|
parentTaskOwnerMap = new HashMap<>();
|
||||||
|
for (ProjectTaskDO p : projectTaskMapper.selectBatchIds(parentTaskIds)) {
|
||||||
|
parentTaskOwnerMap.put(p.getId(), p.getOwnerId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
voList.forEach(vo -> {
|
||||||
|
vo.setOwnerNickname(nicknameMap.get(vo.getOwnerId()));
|
||||||
|
List<TaskAssigneeDO> activeList = assigneeMap.getOrDefault(vo.getId(), List.of());
|
||||||
|
vo.setAssignees(activeList.stream()
|
||||||
|
.map(a -> toAssigneeView(a, nicknameMap.get(a.getUserId())))
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
vo.setTotalSpentHours(spentHoursMap.getOrDefault(vo.getId(), BigDecimal.ZERO));
|
||||||
|
if (vo.getParentTaskId() != null) {
|
||||||
|
vo.setParentTaskOwnerId(parentTaskOwnerMap.get(vo.getParentTaskId()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<ProjectTaskRespVO> assembleTaskRespVOPageCrossExecution(Long projectId, PageResult<ProjectTaskDO> doPage) {
|
||||||
|
if (doPage == null || CollUtil.isEmpty(doPage.getList())) {
|
||||||
|
return new PageResult<>(Collections.emptyList(), doPage == null ? 0L : doPage.getTotal());
|
||||||
|
}
|
||||||
|
List<ProjectTaskDO> taskList = doPage.getList();
|
||||||
|
List<ProjectTaskRespVO> voList = BeanUtils.toBean(taskList, ProjectTaskRespVO.class);
|
||||||
|
|
||||||
|
// 批量回填 ownerNickname / assignees / totalSpentHours / parentTaskOwnerId(与 executionId 无关)
|
||||||
|
enrichTaskRelations(taskList, voList);
|
||||||
|
|
||||||
|
voList.forEach(vo -> {
|
||||||
|
// executionOwnerId 在跨执行场景不设置(涉及多个 execution,无"该执行的 ownerId"概念)
|
||||||
|
try {
|
||||||
|
applyLifecycle(vo);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[assembleTaskRespVOPageCrossExecution] applyLifecycle 装配失败 taskId={}", vo.getId(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按 task.executionId 分组批量回填 executionName / executionStatusCode
|
||||||
|
enrichExecutionInfo(voList);
|
||||||
|
|
||||||
|
return new PageResult<>(voList, doPage.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给一批 ProjectTaskRespVO 批量回填 executionName / executionStatusCode。
|
||||||
|
* 一次查询所有涉及的 executionId,避免 N+1。
|
||||||
|
*/
|
||||||
|
private void enrichExecutionInfo(List<ProjectTaskRespVO> list) {
|
||||||
|
if (CollUtil.isEmpty(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<Long> executionIds = list.stream()
|
||||||
|
.map(ProjectTaskRespVO::getExecutionId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (executionIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ProjectExecutionDO> executions = projectExecutionMapper.selectBatchIds(executionIds);
|
||||||
|
Map<Long, ProjectExecutionDO> executionMap = executions.stream()
|
||||||
|
.collect(Collectors.toMap(ProjectExecutionDO::getId, e -> e, (a, b) -> a));
|
||||||
|
list.forEach(vo -> {
|
||||||
|
ProjectExecutionDO exec = executionMap.get(vo.getExecutionId());
|
||||||
|
if (exec != null) {
|
||||||
|
vo.setExecutionName(exec.getExecutionName());
|
||||||
|
vo.setExecutionStatusCode(exec.getStatusCode());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把任务 RespVO 上的项目需求信息(TD-013)回填。
|
* 把任务 RespVO 上的项目需求信息(TD-013)回填。
|
||||||
* <p>本服务的 task 查询入口均以 executionId 为收口(详情 / page / board-page 共用 assembleTaskRespVOPage),
|
* <p>本服务的 task 查询入口均以 executionId 为收口(详情 / page / board-page 共用 assembleTaskRespVOPage),
|
||||||
@@ -628,7 +712,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
* - 进入终态(toStatus.terminalFlag=true)且未填写时,写入 actualEndDate
|
* - 进入终态(toStatus.terminalFlag=true)且未填写时,写入 actualEndDate
|
||||||
*/
|
*/
|
||||||
private void maybeFillActualDates(ProjectTaskDO task, String fromStatus, String toStatus) {
|
private void maybeFillActualDates(ProjectTaskDO task, String fromStatus, String toStatus) {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now(SERVER_ZONE);
|
||||||
LocalDate newActualStart = null;
|
LocalDate newActualStart = null;
|
||||||
LocalDate newActualEnd = null;
|
LocalDate newActualEnd = null;
|
||||||
if (task.getActualStartDate() == null) {
|
if (task.getActualStartDate() == null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user