diff --git a/CLAUDE.md b/CLAUDE.md index a322719..a751836 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,7 @@ - **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。 - **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。 - **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。 -- 列表/详情这类对象内**读路径**目前未挂 `@CheckObjectPermission`(属已识别负债,台账 TD-001),新增读接口暂沿用现状即可,不要顺手改造,等独立立项。 +- 对象内**读路径**(列表 / 详情 / 状态看板 / 聚合)已统一在 **Service 层**挂 `@CheckObjectPermission(objectType=PROJECT, permission=...PERMISSION_QUERY)`——查询同样要扫库耗资源,必须按对象域鉴权(原台账 TD-001 所述"读路径未挂"已不成立)。**Controller 方法层一律不挂权限注解**,对象域鉴权全部落 Service;新增读接口照此在 Service 层挂对象域权限,不要只在 Controller 留空、更不要误判"Controller 没注解 = 无鉴权"。 判定口诀:**URL 里有 `{projectId}` / `{productId}` 等对象 ID → 对象域;没有 → 全域**。 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectExecutionConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectExecutionConstants.java index 699979e..05d7e16 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectExecutionConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectExecutionConstants.java @@ -20,6 +20,13 @@ public final class ProjectExecutionConstants { */ public static final String BIZ_TYPE = "project_execution"; + /** + * 执行读路径查询权限码(对象域,object_type='project')。 + * 覆盖执行对象所有读路径:page / status-board / detail。 + * "我参与 / 所有"视角由前端发不发 involveUserId 决定;进得来 = 看项目下全部,无此权限码直接 403。 + */ + public static final String PERMISSION_QUERY = "project:execution:query"; + /** * 创建执行权限码。 */ diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectTaskConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectTaskConstants.java index 0bf93ed..45eebcb 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectTaskConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectTaskConstants.java @@ -26,6 +26,13 @@ public final class ProjectTaskConstants { */ public static final String BIZ_TYPE = "project_task"; + /** + * 任务读路径查询权限码(对象域,object_type='project')。 + * 覆盖任务对象所有读路径:page / status-board / board-page / detail / summary(含跨执行 aggregate)。 + * "我参与 / 所有"视角由前端发不发 involveUserId 决定;进得来 = 看项目下全部,无此权限码直接 403。 + */ + public static final String PERMISSION_QUERY = "project:task:query"; + /** * 创建任务权限码。 */ @@ -59,18 +66,6 @@ public final class ProjectTaskConstants { */ 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" 与中文 "删除",前端可纯中文文案。 * 校验时精确匹配(trim 后比对)。 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionPageReqVO.java index a903d8b..4c7ea08 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionPageReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionPageReqVO.java @@ -23,7 +23,10 @@ public class ProjectExecutionPageReqVO extends PageParam { @Size(max = 32, message = "执行类型长度不能超过32个字符") private String executionType; - @Schema(description = "执行负责人用户编号", example = "3001") + @Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 项目内全部执行") + private Long involveUserId; + + @Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3001") private Long ownerId; @Schema(description = "执行状态编码", example = "pending") @@ -38,4 +41,8 @@ public class ProjectExecutionPageReqVO extends PageParam { @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] updateTime; + @Schema(description = "截止时间范围 chip:overdue(逾期)/ today(今天到期)/ thisWeek(本周到期);" + + "基于 plannedEndDate 且排除终态执行;不传 = 不按截止时间过滤", example = "overdue") + private String dueRange; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionStatusBoardReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionStatusBoardReqVO.java index 8749b0d..740a7e1 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionStatusBoardReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionStatusBoardReqVO.java @@ -22,11 +22,18 @@ public class ProjectExecutionStatusBoardReqVO { @Size(max = 32, message = "执行类型长度不能超过32个字符") private String executionType; - @Schema(description = "执行负责人用户编号", example = "3001") + @Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 项目内全部执行") + private Long involveUserId; + + @Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3001") private Long ownerId; @Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] updateTime; + @Schema(description = "截止时间范围 chip:overdue(逾期)/ today(今天到期)/ thisWeek(本周到期);" + + "基于 plannedEndDate 且排除终态执行;不传 = 不按截止时间过滤", example = "overdue") + private String dueRange; + } 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 index 0281af3..3947645 100644 --- 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 @@ -57,7 +57,7 @@ public class ProjectTaskAggregateController { } @GetMapping("/summary") - @Operation(summary = "获取项目任务今日小条(支持 scope=mine|all)") + @Operation(summary = "获取项目任务今日小条(involveUserId 控制是否限定 owner / 活跃协办)") public CommonResult getTaskSummary( @PathVariable("projectId") Long projectId, @Valid ProjectTaskSummaryReqVO reqVO) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskBoardPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskBoardPageReqVO.java index 8b4be43..629ad58 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskBoardPageReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskBoardPageReqVO.java @@ -36,7 +36,10 @@ public class ProjectTaskBoardPageReqVO extends PageParam { @Schema(description = "父任务编号", example = "9001") private Long parentTaskId; - @Schema(description = "任务负责人用户编号", example = "3002") + @Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务") + private Long involveUserId; + + @Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002") private Long ownerId; @Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskPageReqVO.java index 07dc61e..bd6ce63 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskPageReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskPageReqVO.java @@ -22,7 +22,10 @@ public class ProjectTaskPageReqVO extends PageParam { @Schema(description = "父任务编号") private Long parentTaskId; - @Schema(description = "任务负责人用户编号", example = "3002") + @Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务") + private Long involveUserId; + + @Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002") private Long ownerId; @Schema(description = "任务状态编码", example = "pending") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskStatusBoardReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskStatusBoardReqVO.java index c6ee197..bc69b95 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskStatusBoardReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskStatusBoardReqVO.java @@ -21,7 +21,10 @@ public class ProjectTaskStatusBoardReqVO { @Schema(description = "父任务编号", example = "9001") private Long parentTaskId; - @Schema(description = "任务负责人用户编号", example = "3002") + @Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务") + private Long involveUserId; + + @Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002") private Long ownerId; @Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]") 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 index d6d1771..f605a3f 100644 --- 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 @@ -20,7 +20,7 @@ public class ProjectTaskAggregateBoardPageReqVO extends PageParam { @Schema(description = "任务名称模糊匹配关键字") private String keyword; - @Schema(description = "限定执行 id 列表;不传 = 项目内全部执行") + @Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行") private List executionIds; @Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤") @@ -29,6 +29,9 @@ public class ProjectTaskAggregateBoardPageReqVO extends PageParam { @Schema(description = "我参与语义;与 ownerId 二选一") private Long involveUserId; + @Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空") + private Long executionInvolveUserId; + @Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一") private Long ownerId; 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 index 192b363..a0621ed 100644 --- 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 @@ -20,7 +20,7 @@ public class ProjectTaskAggregatePageReqVO extends PageParam { @Schema(description = "任务名称模糊匹配关键字") private String keyword; - @Schema(description = "限定执行 id 列表;不传 = 项目内全部执行") + @Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行") private List executionIds; @Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤") @@ -29,6 +29,9 @@ public class ProjectTaskAggregatePageReqVO extends PageParam { @Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一") private Long involveUserId; + @Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空") + private Long executionInvolveUserId; + @Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一") private Long ownerId; 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 index 3c91710..75e8d60 100644 --- 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 @@ -17,7 +17,7 @@ public class ProjectTaskAggregateStatusBoardReqVO { @Schema(description = "任务名称模糊匹配关键字") private String keyword; - @Schema(description = "限定执行 id 列表;不传 = 项目内全部执行") + @Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行") private List executionIds; @Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤") @@ -26,6 +26,9 @@ public class ProjectTaskAggregateStatusBoardReqVO { @Schema(description = "我参与语义;与 ownerId 二选一") private Long involveUserId; + @Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空") + private Long executionInvolveUserId; + @Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一") private Long ownerId; 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 index 50fc8f5..5547c2a 100644 --- 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 @@ -8,12 +8,9 @@ import lombok.Data; public class ProjectTaskSummaryReqVO { /** - * 数字汇总作用域。 - *
    - *
  • {@code mine}(默认):统计当前登录人 owner 或活跃协办的任务
  • - *
  • {@code all}:统计项目内全部任务,要求 {@code project:task:list-all} 权限码
  • - *
+ * 我参与语义:传入的 userId 是 owner 或活跃协办;不传 = 项目内全部任务。 + * 切换"我参与 / 所有"由前端直接控制此字段是否携带,与其他读接口(page / status-board / board-page)契约一致。 */ - @Schema(description = "作用域:mine(默认) / all", example = "mine") - private String scope; + @Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;不传 = 项目内全部") + private Long involveUserId; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeMapper.java index 34101ea..1d34d8e 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeMapper.java @@ -4,8 +4,6 @@ import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO; import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; import java.util.List; @@ -50,23 +48,6 @@ public interface ExecutionAssigneeMapper extends BaseMapperX selectActiveExecutionIdsByProjectIdAndUserId(@Param("projectId") Long projectId, - @Param("userId") Long userId); - /** * 按 execution_id 批量软删执行协办(含已 removed 的历史段)。 */ diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java index 4331a05..962a8f1 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java @@ -1,5 +1,7 @@ package com.njcn.rdms.module.project.dal.mysql.project.execution; +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.mybatis.core.dataobject.BaseDO; import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; @@ -7,12 +9,11 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO; import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO; -import com.njcn.rdms.module.project.service.project.permission.VisibilityScope; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; -import org.springframework.util.StringUtils; +import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.Map; @@ -33,44 +34,97 @@ public interface ProjectExecutionMapper extends BaseMapperX } default PageResult selectPageByProjectId(Long projectId, - VisibilityScope scope, - ProjectExecutionPageReqVO reqVO) { - // 可见性短路:非 seesAll 且无任何可见执行 → 空页,避免后续 IN () SQL - if (!scope.seesAll() && scope.executionIds().isEmpty()) { - return PageResult.empty(); - } - LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); - queryWrapper.eq(ProjectExecutionDO::getProjectId, projectId); - queryWrapper.eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType()); - queryWrapper.eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId()); - queryWrapper.eqIfPresent(ProjectExecutionDO::getStatusCode, reqVO.getStatusCode()); - queryWrapper.eqIfPresent(ProjectExecutionDO::getPriority, reqVO.getPriority()); - queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime()); - queryWrapper.orderByAsc(ProjectExecutionDO::getPriority); - queryWrapper.orderByDesc(BaseDO::getCreateTime); - queryWrapper.orderByDesc(ProjectExecutionDO::getId); - if (StringUtils.hasText(reqVO.getKeyword())) { - queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword())); - } - if (!scope.seesAll()) { - queryWrapper.in(ProjectExecutionDO::getId, scope.executionIds()); - } - return selectPage(reqVO, queryWrapper); + ProjectExecutionPageReqVO reqVO, + List terminalStatusCodes, + LocalDate today, + LocalDate weekStart, + LocalDate weekEnd) { + Page page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize()); + IPage ipage = doSelectPageByProjectId( + projectId, reqVO, terminalStatusCodes, today, weekStart, weekEnd, page); + return new PageResult<>(ipage.getRecords(), ipage.getTotal()); } /** - * 查 userId 在指定项目下,作为 owner 的所有执行 ID。 - * 用于 VisibilityScopeResolver 收集"我是执行负责人"的 scope 来源。 + * 项目下执行分页查询。 + *

SQL 用 @Select 直接控制,主表以别名 t 暴露,EXISTS 子查询用 t.id 关联; + * 避免 LambdaWrapper + .apply 嵌入裸 SQL 时依赖 "MyBatis-Plus 不给主表加别名" 这一实现细节。 + * 与任务侧 {@code ProjectTaskMapper.selectAggregatePageByProjectId} 同款风格。 + * + *

involveUserId 非空时附加 (owner_id = involveUserId OR 活跃协办含 involveUserId); + * 否则该过滤分支跳过("看项目下全部")。 */ - default List selectIdsByProjectIdAndOwnerId(Long projectId, Long userId) { - return selectList(new LambdaQueryWrapperX() - .select(ProjectExecutionDO::getId) - .eq(ProjectExecutionDO::getProjectId, projectId) - .eq(ProjectExecutionDO::getOwnerId, userId)) - .stream() - .map(ProjectExecutionDO::getId) - .toList(); - } + @Select(""" + + """) + IPage doSelectPageByProjectId( + @Param("projectId") Long projectId, + @Param("reqVO") ProjectExecutionPageReqVO reqVO, + @Param("terminalStatusCodes") List terminalStatusCodes, + @Param("today") LocalDate today, + @Param("weekStart") LocalDate weekStart, + @Param("weekEnd") LocalDate weekEnd, + Page page); default List selectListByOwnerId(Long ownerId) { return selectList(new LambdaQueryWrapperX() @@ -90,28 +144,86 @@ public interface ProjectExecutionMapper extends BaseMapperX } default Integer countByProjectIdAndStatusCode(Long projectId, - VisibilityScope scope, ProjectExecutionStatusBoardReqVO reqVO, - String statusCode) { - // 可见性短路:非 seesAll 且无任何可见执行 → 计数 0 - if (!scope.seesAll() && scope.executionIds().isEmpty()) { - return 0; - } - LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() - .eq(ProjectExecutionDO::getProjectId, projectId) - .eq(ProjectExecutionDO::getStatusCode, statusCode) - .eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType()) - .eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId()) - .betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime()); - if (StringUtils.hasText(reqVO.getKeyword())) { - queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword())); - } - if (!scope.seesAll()) { - queryWrapper.in(ProjectExecutionDO::getId, scope.executionIds()); - } - return Math.toIntExact(selectCount(queryWrapper)); + String statusCode, + List terminalStatusCodes, + LocalDate today, + LocalDate weekStart, + LocalDate weekEnd) { + return Math.toIntExact(doCountByProjectIdAndStatusCode( + projectId, reqVO, statusCode, terminalStatusCodes, today, weekStart, weekEnd)); } + /** + * 项目下指定状态的执行计数(与 doSelectPageByProjectId 同款过滤口径)。 + * 同上:用 @Select 显式表别名 t 替代 LambdaWrapper + .apply EXISTS 写法。 + */ + @Select(""" + + """) + Long doCountByProjectIdAndStatusCode( + @Param("projectId") Long projectId, + @Param("reqVO") ProjectExecutionStatusBoardReqVO reqVO, + @Param("statusCode") String statusCode, + @Param("terminalStatusCodes") List terminalStatusCodes, + @Param("today") LocalDate today, + @Param("weekStart") LocalDate weekStart, + @Param("weekEnd") LocalDate weekEnd); + /** * TD-016:按 projectRequirementIds 批量聚合承接执行的平均进度,避免列表 N+1。 *

口径:仅统计 deleted = 0 + status_code NOT IN(excludedStatusCodes) 的执行; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java index aaac4e7..9647e04 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java @@ -3,7 +3,6 @@ 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.mybatis.core.dataobject.BaseDO; import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO; @@ -11,12 +10,10 @@ import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTask 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.service.project.permission.VisibilityScope; import lombok.Data; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; -import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDate; @@ -36,33 +33,68 @@ public interface ProjectTaskMapper extends BaseMapperX { } default PageResult selectPageByExecutionId(Long projectId, Long executionId, - VisibilityScope scope, ProjectTaskPageReqVO reqVO) { - // 可见性短路:非 seesAll 且无任何可见任务 → 空页 - if (!scope.seesAll() && scope.taskIds().isEmpty()) { - return PageResult.empty(); - } - LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); - queryWrapper.eq(ProjectTaskDO::getProjectId, projectId); - queryWrapper.eq(ProjectTaskDO::getExecutionId, executionId); - queryWrapper.eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId()); - queryWrapper.eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId()); - queryWrapper.eqIfPresent(ProjectTaskDO::getStatusCode, reqVO.getStatusCode()); - queryWrapper.eqIfPresent(ProjectTaskDO::getPriority, reqVO.getPriority()); - queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime()); - queryWrapper.orderByAsc(ProjectTaskDO::getParentTaskId); - queryWrapper.orderByAsc(ProjectTaskDO::getPriority); - queryWrapper.orderByDesc(BaseDO::getCreateTime); - queryWrapper.orderByDesc(ProjectTaskDO::getId); - if (StringUtils.hasText(reqVO.getKeyword())) { - queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword())); - } - if (!scope.seesAll()) { - queryWrapper.in(ProjectTaskDO::getId, scope.taskIds()); - } - return selectPage(reqVO, queryWrapper); + Page page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize()); + IPage ipage = doSelectPageByExecutionId(projectId, executionId, reqVO, page); + return new PageResult<>(ipage.getRecords(), ipage.getTotal()); } + /** + * 执行内任务分页查询。 + *

SQL 用 @Select 显式表别名 t,EXISTS 子查询用 t.id 关联 rdms_task_assignee; + * 与项目级 aggregate page 同款风格。 + * + *

involveUserId 非空时附加 (owner_id = involveUserId OR 活跃协办含 involveUserId); + * 与 ownerId 文档标注「二选一」由前端保证(不做后端互斥校验)。 + */ + @Select(""" + + """) + IPage doSelectPageByExecutionId( + @Param("projectId") Long projectId, + @Param("executionId") Long executionId, + @Param("reqVO") ProjectTaskPageReqVO reqVO, + Page page); + default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) { ProjectTaskDO update = new ProjectTaskDO(); update.setStatusCode(toStatus); @@ -274,78 +306,57 @@ public interface ProjectTaskMapper extends BaseMapperX { .eq(ProjectTaskDO::getId, id)); } - /** - * 递归 CTE:从"userId 在指定项目下作为 owner_id 的全部任务"出发,向下展开包含所有子孙的任务 ID。 - * 用于 VisibilityScopeResolver 中"任务负责人 → 自己 + 全部子孙"规则的 scope 收集。 - * - * 任务表已逻辑删除的行不参与递归(WHERE 子句过滤 deleted)。 - * 单棵子树最大深度受 MySQL `cte_max_recursion_depth`(默认 1000)限制,业务实际任务树远低于此。 - */ - @Select(""" - WITH RECURSIVE owned (id) AS ( - SELECT id FROM rdms_task - WHERE deleted = b'0' - AND project_id = #{projectId} - AND owner_id = #{userId} - UNION ALL - SELECT t.id FROM rdms_task t - JOIN owned o ON t.parent_task_id = o.id - WHERE t.deleted = b'0' - ) - SELECT id FROM owned - """) - List selectOwnedTaskAndDescendantIdsByProjectIdAndUserId( - @Param("projectId") Long projectId, - @Param("userId") Long userId); - - /** - * 同 selectOwnedTaskAndDescendantIdsByProjectIdAndUserId 但再加 execution_id 维度。 - * 注意:递归向下展开只跟着 parent_task_id,子任务必然与父任务在同一 execution 下, - * 因此 execution_id 过滤仅作用于种子(owned)那一步即可。 - */ - @Select(""" - WITH RECURSIVE owned (id) AS ( - SELECT id FROM rdms_task - WHERE deleted = b'0' - AND project_id = #{projectId} - AND execution_id = #{executionId} - AND owner_id = #{userId} - UNION ALL - SELECT t.id FROM rdms_task t - JOIN owned o ON t.parent_task_id = o.id - WHERE t.deleted = b'0' - ) - SELECT id FROM owned - """) - List selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId( - @Param("projectId") Long projectId, - @Param("executionId") Long executionId, - @Param("userId") Long userId); - default Integer countByProjectIdAndExecutionIdAndStatusCode(Long projectId, Long executionId, - VisibilityScope scope, ProjectTaskStatusBoardReqVO reqVO, String statusCode) { - // 可见性短路:非 seesAll 且无任何可见任务 → 0 - if (!scope.seesAll() && scope.taskIds().isEmpty()) { - return 0; - } - LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() - .eq(ProjectTaskDO::getProjectId, projectId) - .eq(ProjectTaskDO::getExecutionId, executionId) - .eq(ProjectTaskDO::getStatusCode, statusCode) - .eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId()) - .eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId()) - .betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime()); - if (StringUtils.hasText(reqVO.getKeyword())) { - queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword())); - } - if (!scope.seesAll()) { - queryWrapper.in(ProjectTaskDO::getId, scope.taskIds()); - } - return Math.toIntExact(selectCount(queryWrapper)); + return Math.toIntExact(doCountByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, reqVO, statusCode)); } + /** + * 执行内任务按状态计数(与 doSelectPageByExecutionId 同款过滤口径,含 involveUserId 协办分支)。 + */ + @Select(""" + + """) + Long doCountByProjectIdAndExecutionIdAndStatusCode( + @Param("projectId") Long projectId, + @Param("executionId") Long executionId, + @Param("reqVO") ProjectTaskStatusBoardReqVO reqVO, + @Param("statusCode") String statusCode); + /** * 收集执行下的所有任务 id(含子孙——子孙必然同 execution_id,所以一把抓即可)。 * 用于"删除执行"时的级联软删。 @@ -364,7 +375,7 @@ public interface ProjectTaskMapper extends BaseMapperX { /** * 从给定任务出发,递归向下收集自身 + 所有子孙任务 id(递归 CTE)。 - * 用于"删除任务"时的级联软删。复用与 selectOwnedTaskAndDescendantIdsByProjectIdAndUserId 同款 CTE 模式。 + * 用于"删除任务"时的级联软删。 * * 任务表已逻辑删除的行不参与递归。 */ @@ -410,8 +421,6 @@ public interface ProjectTaskMapper extends BaseMapperX { * 项目级跨执行任务分页查询。 * * 语义: - * - 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 非空 → 排除终态 @@ -423,13 +432,6 @@ public interface ProjectTaskMapper extends BaseMapperX { t.project_id = #{projectId} AND t.deleted = b'0' - - AND t.id IN - #{tid} - - - AND 1=0 - AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%') @@ -446,6 +448,23 @@ public interface ProjectTaskMapper extends BaseMapperX { #{esc} ) + + AND EXISTS ( + SELECT 1 FROM rdms_project_execution e + WHERE e.id = t.execution_id + AND e.deleted = b'0' + AND ( + e.owner_id = #{reqVO.executionInvolveUserId} + OR EXISTS ( + SELECT 1 FROM rdms_execution_assignee ea + WHERE ea.execution_id = e.id + AND ea.user_id = #{reqVO.executionInvolveUserId} + AND ea.removed_at IS NULL + AND ea.deleted = b'0' + ) + ) + ) + AND ( t.owner_id = #{reqVO.involveUserId} @@ -503,8 +522,6 @@ public interface ProjectTaskMapper extends BaseMapperX { """) IPage selectAggregatePageByProjectId( @Param("projectId") Long projectId, - @Param("seesAll") boolean seesAll, - @Param("visibleTaskIds") java.util.Collection visibleTaskIds, @Param("reqVO") ProjectTaskAggregatePageReqVO reqVO, @Param("terminalStatusCodes") Collection terminalStatusCodes, @Param("weekStart") LocalDate weekStart, @@ -523,13 +540,6 @@ public interface ProjectTaskMapper extends BaseMapperX { t.project_id = #{projectId} AND t.deleted = b'0' - - AND t.id IN - #{tid} - - - AND 1=0 - AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%') @@ -546,6 +556,23 @@ public interface ProjectTaskMapper extends BaseMapperX { #{esc} ) + + AND EXISTS ( + SELECT 1 FROM rdms_project_execution e + WHERE e.id = t.execution_id + AND e.deleted = b'0' + AND ( + e.owner_id = #{reqVO.executionInvolveUserId} + OR EXISTS ( + SELECT 1 FROM rdms_execution_assignee ea + WHERE ea.execution_id = e.id + AND ea.user_id = #{reqVO.executionInvolveUserId} + AND ea.removed_at IS NULL + AND ea.deleted = b'0' + ) + ) + ) + AND ( t.owner_id = #{reqVO.involveUserId} @@ -589,8 +616,6 @@ public interface ProjectTaskMapper extends BaseMapperX { """) List selectAggregateStatusCount( @Param("projectId") Long projectId, - @Param("seesAll") boolean seesAll, - @Param("visibleTaskIds") java.util.Collection visibleTaskIds, @Param("reqVO") ProjectTaskAggregateStatusBoardReqVO reqVO, @Param("terminalStatusCodes") Collection terminalStatusCodes, @Param("weekStart") LocalDate weekStart, @@ -634,13 +659,6 @@ public interface ProjectTaskMapper extends BaseMapperX { t.project_id = #{projectId} AND t.deleted = b'0' - - AND t.id IN - #{tid} - - - AND 1=0 - AND ( t.owner_id = #{involveUserId} @@ -658,8 +676,6 @@ public interface ProjectTaskMapper extends BaseMapperX { """) Map selectAggregateSummaryCounts( @Param("projectId") Long projectId, - @Param("seesAll") boolean seesAll, - @Param("visibleTaskIds") java.util.Collection visibleTaskIds, @Param("involveUserId") Long involveUserId, @Param("terminalStatusCodes") Collection terminalStatusCodes, @Param("completedStatusCode") String completedStatusCode, diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskAssigneeMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskAssigneeMapper.java index 111807c..2ef3bb5 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskAssigneeMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskAssigneeMapper.java @@ -4,8 +4,6 @@ import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO; import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; import java.util.Collection; import java.util.Collections; @@ -50,41 +48,6 @@ public interface TaskAssigneeMapper extends BaseMapperX { .orderByAsc(TaskAssigneeDO::getId)); } - /** - * 查 userId 在指定项目下,当前活跃协办的所有任务 ID(removed_at IS NULL)。 - * 走 JOIN 是因为 task_assignee 表没有 project_id 冗余字段。 - * 用于 VisibilityScopeResolver 收集"我是任务协办人"的 scope 来源(项目维度)。 - */ - @Select(""" - SELECT a.task_id - FROM rdms_task_assignee a - JOIN rdms_task t ON t.id = a.task_id AND t.deleted = b'0' - WHERE a.deleted = b'0' - AND a.removed_at IS NULL - AND t.project_id = #{projectId} - AND a.user_id = #{userId} - """) - List selectActiveTaskIdsByProjectIdAndUserId(@Param("projectId") Long projectId, - @Param("userId") Long userId); - - /** - * 同上,但再加 execution_id 维度,用于"任务分页(执行内)"的 scope。 - */ - @Select(""" - SELECT a.task_id - FROM rdms_task_assignee a - JOIN rdms_task t ON t.id = a.task_id AND t.deleted = b'0' - WHERE a.deleted = b'0' - AND a.removed_at IS NULL - AND t.project_id = #{projectId} - AND t.execution_id = #{executionId} - AND a.user_id = #{userId} - """) - List selectActiveTaskIdsByProjectIdAndExecutionIdAndUserId( - @Param("projectId") Long projectId, - @Param("executionId") Long executionId, - @Param("userId") Long userId); - /** * 按主键 + 任务 ID 双键查;返回的记录可能已失效(removed_at != null),由调用方判断。 */ diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ObjectPermissionService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ObjectPermissionService.java index c63afad..6721d43 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ObjectPermissionService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ObjectPermissionService.java @@ -26,7 +26,7 @@ public interface ObjectPermissionService { * 本方法不抛异常,纯返回 boolean,用于"无权限就走降级路径"而非"无权限就 403"的场景。 * * @param objectId 对象 ID(如 projectId) - * @param permission 权限码,如 {@code project:task:list-all} + * @param permission 权限码,如 {@code project:task:query} * @return true=具备,false=不具备 */ boolean hasPermission(Long objectId, String permission); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceImpl.java index b452d8e..0532818 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceImpl.java @@ -1,9 +1,10 @@ package com.njcn.rdms.module.project.service.project; import com.njcn.rdms.framework.common.pojo.PageResult; -import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.module.project.constant.ProjectExecutionConstants; +import com.njcn.rdms.module.project.constant.ProjectObjectConstants; import com.njcn.rdms.module.project.constant.ProjectTaskConstants; +import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardRespVO; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageReqVO; @@ -12,18 +13,17 @@ import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTask import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO; -import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO; 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.execution.ProjectExecutionMapper; 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.service.project.permission.VisibilityScope; -import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver; import com.njcn.rdms.module.project.service.project.task.ProjectTaskService; +import com.njcn.rdms.module.project.util.DueRangeSupport; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; +import java.time.LocalDate; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; @@ -42,28 +42,28 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService @Resource private ProjectTaskMapper projectTaskMapper; @Resource - private VisibilityScopeResolver visibilityScopeResolver; - @Resource private ProjectTaskService projectTaskService; @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectExecutionConstants.PERMISSION_QUERY) public ProjectExecutionStatusBoardRespVO getExecutionStatusBoard(Long projectId, ProjectExecutionStatusBoardReqVO reqVO) { - Long userId = SecurityFrameworkUtils.getLoginUserId(); - VisibilityScope scope = visibilityScopeResolver.resolveForProject(projectId, userId); List statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE); - return buildExecutionStatusBoard(projectId, scope, reqVO, statusModels); + return buildExecutionStatusBoard(projectId, reqVO, statusModels); } @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) public ProjectTaskStatusBoardRespVO getTaskStatusBoard(Long projectId, Long executionId, ProjectTaskStatusBoardReqVO reqVO) { - VisibilityScope scope = resolveTaskScope(projectId, executionId); List statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); - return buildTaskStatusBoard(projectId, executionId, scope, reqVO, statusModels); + return buildTaskStatusBoard(projectId, executionId, reqVO, statusModels); } @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) public ProjectTaskBoardPageRespVO getTaskBoardPage(Long projectId, Long executionId, ProjectTaskBoardPageReqVO reqVO) { - VisibilityScope scope = resolveTaskScope(projectId, executionId); List statusModels = objectStatusModelMapper .selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); @@ -77,7 +77,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService ProjectTaskBoardPageRespVO respVO = new ProjectTaskBoardPageRespVO(); List items = targetStatusModels.stream() - .map(statusModel -> buildBoardColumn(projectId, executionId, scope, reqVO, statusModel)) + .map(statusModel -> buildBoardColumn(projectId, executionId, reqVO, statusModel)) .collect(Collectors.toList()); respVO.setItems(items); return respVO; @@ -98,11 +98,10 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService } private ProjectTaskBoardPageRespVO.ColumnItemVO buildBoardColumn(Long projectId, Long executionId, - VisibilityScope scope, ProjectTaskBoardPageReqVO reqVO, ObjectStatusModelDO statusModel) { ProjectTaskPageReqVO innerReq = toInnerPageReq(reqVO, statusModel.getStatusCode()); - PageResult doPage = projectTaskMapper.selectPageByExecutionId(projectId, executionId, scope, innerReq); + PageResult doPage = projectTaskMapper.selectPageByExecutionId(projectId, executionId, innerReq); PageResult voPage = projectTaskService.assembleTaskRespVOPage(projectId, executionId, doPage); ProjectTaskBoardPageRespVO.ColumnItemVO item = new ProjectTaskBoardPageRespVO.ColumnItemVO(); @@ -124,6 +123,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService innerReq.setPageSize(reqVO.getPageSize()); innerReq.setKeyword(reqVO.getKeyword()); innerReq.setParentTaskId(reqVO.getParentTaskId()); + innerReq.setInvolveUserId(reqVO.getInvolveUserId()); innerReq.setOwnerId(reqVO.getOwnerId()); innerReq.setPriority(reqVO.getPriority()); innerReq.setUpdateTime(reqVO.getUpdateTime()); @@ -131,34 +131,24 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService return innerReq; } - /** - * 计算任务可见性 scope,与 ProjectTaskServiceImpl#computeTaskScope 同款: - * 项目经理 → seesAll;执行负责人 = 当前用户 → seesAll;否则按 resolveForExecution 求并集。 - */ - private VisibilityScope resolveTaskScope(Long projectId, Long executionId) { - Long userId = SecurityFrameworkUtils.getLoginUserId(); - VisibilityScope scope = visibilityScopeResolver.resolveForExecution(projectId, executionId, userId); - if (scope.seesAll()) { - return scope; - } - ProjectExecutionDO exec = projectExecutionMapper.selectByProjectIdAndId(projectId, executionId); - if (exec != null && Objects.equals(exec.getOwnerId(), userId)) { - return VisibilityScope.all(); - } - return scope; - } - private ProjectExecutionStatusBoardRespVO buildExecutionStatusBoard(Long projectId, - VisibilityScope scope, ProjectExecutionStatusBoardReqVO reqVO, List statusModels) { + // dueRange 截止时间过滤所需的日期边界与执行终态码(终态排除口径对齐任务 summary)。 + LocalDate today = DueRangeSupport.today(); + LocalDate weekStart = DueRangeSupport.weekStart(today); + LocalDate weekEnd = DueRangeSupport.weekEnd(today); + List terminalStatusCodes = objectStatusModelMapper + .selectTerminalStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE); + ProjectExecutionStatusBoardRespVO respVO = new ProjectExecutionStatusBoardRespVO(); List items = statusModels.stream().map(statusModel -> { ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO item = new ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO(); item.setStatusCode(statusModel.getStatusCode()); item.setStatusName(statusModel.getStatusName()); - item.setCount(projectExecutionMapper.countByProjectIdAndStatusCode(projectId, scope, reqVO, statusModel.getStatusCode()).longValue()); + item.setCount(projectExecutionMapper.countByProjectIdAndStatusCode(projectId, reqVO, + statusModel.getStatusCode(), terminalStatusCodes, today, weekStart, weekEnd).longValue()); item.setSort(statusModel.getSort()); item.setTerminal(statusModel.getTerminalFlag()); return item; @@ -169,7 +159,6 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService } private ProjectTaskStatusBoardRespVO buildTaskStatusBoard(Long projectId, Long executionId, - VisibilityScope scope, ProjectTaskStatusBoardReqVO reqVO, List statusModels) { ProjectTaskStatusBoardRespVO respVO = new ProjectTaskStatusBoardRespVO(); @@ -177,7 +166,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO item = new ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO(); item.setStatusCode(statusModel.getStatusCode()); item.setStatusName(statusModel.getStatusName()); - item.setCount(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, scope, reqVO, + item.setCount(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, reqVO, statusModel.getStatusCode()).longValue()); item.setSort(statusModel.getSort()); item.setTerminal(statusModel.getTerminalFlag()); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java index 46d4c33..98ef254 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java @@ -41,11 +41,10 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; +import com.njcn.rdms.module.project.util.DueRangeSupport; import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission; import com.njcn.rdms.module.project.service.project.ProjectRequirementService; import com.njcn.rdms.module.project.service.project.ProjectService; -import com.njcn.rdms.module.project.service.project.permission.VisibilityScope; -import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver; import com.njcn.rdms.module.project.service.project.task.ProjectTaskService; import com.njcn.rdms.module.system.api.dict.DictDataApi; import com.njcn.rdms.module.system.api.user.AdminUserApi; @@ -119,8 +118,6 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { private ProjectRequirementService projectRequirementService; @Resource private ProjectRequirementMapper projectRequirementMapper; - @Resource - private VisibilityScopeResolver visibilityScopeResolver; /** * 任务服务:执行 cancel / pause / resume 时级联调任务侧 internal 入口。 * 与 ProjectTaskService 互相依赖(任务侧已注入 ProjectExecutionService 用于通知执行),用 @Lazy 打破循环。 @@ -209,24 +206,25 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { } @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectExecutionConstants.PERMISSION_QUERY) public PageResult getExecutionPage(Long projectId, ProjectExecutionPageReqVO reqVO) { validateProjectExists(projectId); - // 数据可见性:项目经理看全部;非经理按"我 owner 的执行 ∪ 我活跃协办的执行"过滤 - Long userId = SecurityFrameworkUtils.getLoginUserId(); - VisibilityScope scope = visibilityScopeResolver.resolveForProject(projectId, userId); - return projectExecutionMapper.selectPageByProjectId(projectId, scope, reqVO); + // "我参与 / 所有"视角完全由前端发不发 reqVO.involveUserId 决定。 + // 注:getExecutionRespVOPage 内部 this.getExecutionPage() 自调用不触发 AOP,但外层注解已守门; + // 此处独立挂注解是为了堵跨 service 直调 ProjectExecutionService.getExecutionPage 的鉴权后门。 + // dueRange 截止时间过滤所需的日期边界与执行终态码(终态排除口径对齐任务 summary)。 + LocalDate today = DueRangeSupport.today(); + return projectExecutionMapper.selectPageByProjectId(projectId, reqVO, + loadExecutionTerminalStatusCodes(), today, + DueRangeSupport.weekStart(today), DueRangeSupport.weekEnd(today)); } @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectExecutionConstants.PERMISSION_QUERY) public ProjectExecutionRespVO getExecutionRespVO(Long projectId, Long executionId) { ProjectExecutionDO execution = getExecution(projectId, executionId); - // 可见性卡断:项目经理放行;否则 executionId 必须在 scope.executionIds 中。 - // 未命中按"执行不存在"语义返回,不暴露存在性。 - Long userId = SecurityFrameworkUtils.getLoginUserId(); - VisibilityScope scope = visibilityScopeResolver.resolveForProject(projectId, userId); - if (!scope.seesAll() && !scope.executionIds().contains(executionId)) { - throw exception(ErrorCodeConstants.PROJECT_EXECUTION_NOT_EXISTS); - } ProjectExecutionRespVO respVO = BeanUtils.toBean(execution, ProjectExecutionRespVO.class); respVO.setProgressRate(loadExecutionProgress(projectId, executionId)); boolean rootTasksAllCompleted = loadExecutionRootTasksAllCompleted(projectId, executionId); @@ -237,6 +235,8 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { } @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectExecutionConstants.PERMISSION_QUERY) public PageResult getExecutionRespVOPage(Long projectId, ProjectExecutionPageReqVO reqVO) { PageResult pageResult = getExecutionPage(projectId, reqVO); PageResult voPageResult = BeanUtils.toBean(pageResult, ProjectExecutionRespVO.class); @@ -859,6 +859,13 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { return statusCodes == null ? Collections.emptyList() : statusCodes; } + /** dueRange 终态排除用:执行对象域的终态码(动态查,不硬编码)。 */ + private List loadExecutionTerminalStatusCodes() { + List statusCodes = objectStatusModelMapper + .selectTerminalStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE); + return statusCodes == null ? Collections.emptyList() : statusCodes; + } + /** * 执行详情完成态:判断"参与聚合的根任务"是否全部为 completed。 * 筛选口径与 loadExecutionProgress 同源;空集(无参与聚合的根任务)返回 false,禁止下发 complete。 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScope.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScope.java deleted file mode 100644 index fc7f130..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScope.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.njcn.rdms.module.project.service.project.permission; - -import java.util.Set; - -/** - * 数据可见性 scope,由 VisibilityScopeResolver 计算得出。 - * - seesAll=true:项目经理等"看全部"角色,分页/计数 SQL 跳过任何 ID 过滤 - * - seesAll=false:仅命中 executionIds / taskIds 的数据可见;集合为空 = 完全不可见 - * - * 实例不可变;空集合用 Set.of() 表达,调用方不得修改。 - */ -public record VisibilityScope( - boolean seesAll, - Set executionIds, - Set taskIds -) { - - public static VisibilityScope all() { - return new VisibilityScope(true, Set.of(), Set.of()); - } - - public static VisibilityScope of(Set executionIds, Set taskIds) { - return new VisibilityScope(false, - executionIds == null ? Set.of() : Set.copyOf(executionIds), - taskIds == null ? Set.of() : Set.copyOf(taskIds)); - } - - public static VisibilityScope empty() { - return new VisibilityScope(false, Set.of(), Set.of()); - } -} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolver.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolver.java deleted file mode 100644 index a29df15..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolver.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.njcn.rdms.module.project.service.project.permission; - -/** - * 计算当前登录用户在某项目 / 某执行下的数据可见性 scope。 - * - * 规则: - * - 项目经理(project.manager_user_id == userId)→ seesAll=true - * - 非项目经理 → 取以下 4 项的并集,构成 (executionIds, taskIds): - * 1. 我作为 execution.owner_id 的执行 ID - * 2. 我作为 execution_assignee 活跃协办的执行 ID(removed_at IS NULL) - * 3. 我作为 task.owner_id 的任务 ID 及其全部子孙 ID(递归 CTE 一次展开) - * 4. 我作为 task_assignee 活跃协办的任务 ID(removed_at IS NULL) - * - * 任务参与者集合 ⊆ 执行参与者集合(业务约束:任务负责人/协办人必须从执行团队挑选)。 - */ -public interface VisibilityScopeResolver { - - /** - * 项目维度 scope(用于执行分页 / 执行看板)。 - */ - VisibilityScope resolveForProject(Long projectId, Long userId); - - /** - * 执行维度 scope(用于任务分页 / 任务看板 / 任务详情)。 - * 调用方需先保证 executionId 属于 projectId(由 URL 路径约束)。 - */ - VisibilityScope resolveForExecution(Long projectId, Long executionId, Long userId); -} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolverImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolverImpl.java deleted file mode 100644 index 2eb2d12..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolverImpl.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.njcn.rdms.module.project.service.project.permission; - -import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; -import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; -import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper; -import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper; -import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper; -import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper; -import jakarta.annotation.Resource; -import org.springframework.stereotype.Service; - -import java.util.LinkedHashSet; -import java.util.Objects; -import java.util.Set; - -/** - * VisibilityScopeResolver 实现。 - * - * 短路:project.manager_user_id == userId → seesAll=true,跳过任何 ID 过滤。 - * 非项目经理:并集 4 个 Mapper 来源得到可见的 executionIds / taskIds。 - * - * 任务的"执行 owner 看执行下所有任务"短路不在此处实现, - * 由 ProjectTaskServiceImpl / ProjectStatusBoardServiceImpl 在调用本 Resolver 前自行判定。 - * 本 Resolver 仅负责"参与者 → 可见 ID 集合"的纯查询。 - */ -@Service -public class VisibilityScopeResolverImpl implements VisibilityScopeResolver { - - @Resource - private ProjectMapper projectMapper; - @Resource - private ProjectExecutionMapper projectExecutionMapper; - @Resource - private ExecutionAssigneeMapper executionAssigneeMapper; - @Resource - private ProjectTaskMapper projectTaskMapper; - @Resource - private TaskAssigneeMapper taskAssigneeMapper; - - @Override - public VisibilityScope resolveForProject(Long projectId, Long userId) { - if (isProjectManager(projectId, userId)) { - return VisibilityScope.all(); - } - Set executionIds = new LinkedHashSet<>(); - executionIds.addAll(projectExecutionMapper.selectIdsByProjectIdAndOwnerId(projectId, userId)); - executionIds.addAll(executionAssigneeMapper.selectActiveExecutionIdsByProjectIdAndUserId(projectId, userId)); - - Set taskIds = new LinkedHashSet<>(); - taskIds.addAll(projectTaskMapper.selectOwnedTaskAndDescendantIdsByProjectIdAndUserId(projectId, userId)); - taskIds.addAll(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndUserId(projectId, userId)); - - return VisibilityScope.of(executionIds, taskIds); - } - - @Override - public VisibilityScope resolveForExecution(Long projectId, Long executionId, Long userId) { - if (isProjectManager(projectId, userId)) { - return VisibilityScope.all(); - } - // executionIds 在执行维度无用,统一传空集;调用方靠 taskIds 过滤分页/计数。 - Set taskIds = new LinkedHashSet<>(); - taskIds.addAll(projectTaskMapper.selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId(projectId, executionId, userId)); - taskIds.addAll(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndExecutionIdAndUserId(projectId, executionId, userId)); - - return VisibilityScope.of(Set.of(), taskIds); - } - - private boolean isProjectManager(Long projectId, Long userId) { - if (projectId == null || userId == null) { - return false; - } - ProjectDO project = projectMapper.selectById(projectId); - return project != null && Objects.equals(project.getManagerUserId(), userId); - } -} 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 index 7c9e7bd..f4652df 100644 --- 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 @@ -23,8 +23,10 @@ public interface ProjectTaskAggregateService { ProjectTaskBoardPageRespVO getAggregateTaskBoardPage(Long projectId, ProjectTaskAggregateBoardPageReqVO reqVO); /** - * scope=mine(默认): 当前用户 owner 或活跃协办; - * scope=all: 全项目任务,要求 project:task:list-all 权限码,否则抛 PROJECT_OBJECT_PERMISSION_DENIED。 + * 跨执行任务今日小条: + * 入参 involveUserId 为 null → 项目内全部任务; + * involveUserId 不为 null → 限定 owner 或活跃协办为该用户。 + * 由 @CheckObjectPermission(project:task:query) 守门,无权限直接 403。 */ 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 index 64bf6e3..733a8c2 100644 --- 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 @@ -3,7 +3,7 @@ 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.ProjectObjectConstants; 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; @@ -17,9 +17,7 @@ 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 com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -47,10 +45,6 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ private ProjectTaskService projectTaskService; @Resource private ObjectStatusModelMapper objectStatusModelMapper; - @Resource - private VisibilityScopeResolver visibilityScopeResolver; - @Resource - private ProjectObjectPermissionService projectObjectPermissionService; // ========= 公共 helper ========= @@ -70,38 +64,17 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ 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 + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) public PageResult getAggregateTaskPage(Long projectId, ProjectTaskAggregatePageReqVO reqVO) { - // 空数组语义短路:前端明确"按 0 个执行状态过滤" → 返空集合,不让 MyBatis 退化成"不过滤" - if (reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) { + // 空数组语义短路:前端明确"按 0 个执行状态/执行 id 过滤" → 返空集合,不让 MyBatis 退化成"不过滤" + if ((reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) + || (reqVO.getExecutionIds() != null && reqVO.getExecutionIds().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); @@ -109,7 +82,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ Page page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize()); IPage ipage = projectTaskMapper.selectAggregatePageByProjectId( - projectId, scope.seesAll(), scope.taskIds(), reqVO, terminalStatusCodes, weekStart, weekEnd, today, page); + projectId, reqVO, terminalStatusCodes, weekStart, weekEnd, today, page); PageResult doPage = new PageResult<>(ipage.getRecords(), ipage.getTotal()); return projectTaskService.assembleTaskRespVOPageCrossExecution(projectId, doPage); } @@ -117,15 +90,16 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ // ========= status-board ========= @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) public ProjectTaskStatusBoardRespVO getAggregateTaskStatusBoard(Long projectId, ProjectTaskAggregateStatusBoardReqVO reqVO) { // 空数组语义短路:跳过 SQL,但保留状态列骨架(前端看板依赖全列表渲染),count 全部 0 - if (reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) { + if ((reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) + || (reqVO.getExecutionIds() != null && reqVO.getExecutionIds().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); @@ -135,7 +109,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); List rows = projectTaskMapper.selectAggregateStatusCount( - projectId, scope.seesAll(), scope.taskIds(), reqVO, terminalStatusCodes, weekStart, weekEnd, today); + projectId, reqVO, terminalStatusCodes, weekStart, weekEnd, today); Map countMap = rows.stream() .collect(Collectors.toMap( ProjectTaskMapper.StatusCountRow::getStatusCode, @@ -173,9 +147,12 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ // ========= board-page ========= @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) public ProjectTaskBoardPageRespVO getAggregateTaskBoardPage(Long projectId, ProjectTaskAggregateBoardPageReqVO reqVO) { - // 空数组语义短路:跳过 SQL,保留按 statusCodes 过滤后的列骨架,每列 list=[] / total=0 - boolean emptyExecStatusCodes = reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty(); + // 空数组语义短路:executionStatusCodes 或 executionIds 为空数组 → 该范围明确为空,跳过 SQL,保留列骨架(每列 list=[] / total=0) + boolean emptyExecScope = (reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty()) + || (reqVO.getExecutionIds() != null && reqVO.getExecutionIds().isEmpty()); List allStatusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE); @@ -189,7 +166,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ .collect(Collectors.toList()); } - if (emptyExecStatusCodes) { + if (emptyExecScope) { List emptyItems = targetStatusModels.stream() .map(sm -> buildColumnItemVO(sm, PageResult.empty())) .collect(Collectors.toList()); @@ -198,15 +175,13 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ 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, + .map(sm -> buildAggregateBoardColumn(projectId, reqVO, sm, terminalStatusCodes, today, weekStart, weekEnd)) .collect(Collectors.toList()); @@ -216,7 +191,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ } private ProjectTaskBoardPageRespVO.ColumnItemVO buildAggregateBoardColumn( - Long projectId, VisibilityScope scope, + Long projectId, ProjectTaskAggregateBoardPageReqVO reqVO, ObjectStatusModelDO sm, List terminalStatusCodes, LocalDate today, LocalDate weekStart, LocalDate weekEnd) { @@ -228,6 +203,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ innerReq.setExecutionIds(reqVO.getExecutionIds()); innerReq.setExecutionStatusCodes(reqVO.getExecutionStatusCodes()); innerReq.setInvolveUserId(reqVO.getInvolveUserId()); + innerReq.setExecutionInvolveUserId(reqVO.getExecutionInvolveUserId()); innerReq.setOwnerId(reqVO.getOwnerId()); innerReq.setStatusCodes(Collections.singletonList(sm.getStatusCode())); innerReq.setPriority(reqVO.getPriority()); @@ -238,7 +214,7 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ Page page = new Page<>(innerReq.getPageNo(), innerReq.getPageSize()); IPage ipage = projectTaskMapper.selectAggregatePageByProjectId( - projectId, scope.seesAll(), scope.taskIds(), innerReq, terminalStatusCodes, weekStart, weekEnd, today, page); + projectId, innerReq, terminalStatusCodes, weekStart, weekEnd, today, page); PageResult doPage = new PageResult<>(ipage.getRecords(), ipage.getTotal()); PageResult voPage = projectTaskService.assembleTaskRespVOPageCrossExecution(projectId, doPage); @@ -261,26 +237,17 @@ public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateServ // ========= summary ========= @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) 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, + projectId, + reqVO.getInvolveUserId(), terminalStatusCodes, ProjectTaskConstants.STATUS_COMPLETED, today, weekStart, weekEnd); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java index e46bdc2..51b1d3c 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java @@ -45,8 +45,6 @@ import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPer import com.njcn.rdms.module.project.framework.security.service.ProjectObjectAuthorizationService; import com.njcn.rdms.module.project.service.project.ProjectService; import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService; -import com.njcn.rdms.module.project.service.project.permission.VisibilityScope; -import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver; import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeService; import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogService; import com.google.common.annotations.VisibleForTesting; @@ -128,8 +126,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { @Resource private ProjectObjectAuthorizationService projectObjectAuthorizationService; @Resource - private VisibilityScopeResolver visibilityScopeResolver; - @Resource private DictDataApi dictDataApi; @Override @@ -432,44 +428,23 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { } @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) public PageResult getTaskPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) { - ProjectExecutionDO execution = validateExecutionExists(projectId, executionId); - VisibilityScope scope = computeTaskScope(projectId, executionId, execution); - return projectTaskMapper.selectPageByExecutionId(projectId, executionId, scope, reqVO); - } - - /** - * 任务可见性计算: - * - 项目经理 → seesAll(由 Resolver 内置判定) - * - 执行负责人 = 当前用户 → seesAll(看本执行下全部任务) - * - 否则 → resolveForExecution 求并集(我 owner 的任务及子孙 ∪ 我活跃协办的任务) - */ - private VisibilityScope computeTaskScope(Long projectId, Long executionId, ProjectExecutionDO execution) { - Long userId = SecurityFrameworkUtils.getLoginUserId(); - VisibilityScope scope = visibilityScopeResolver.resolveForExecution(projectId, executionId, userId); - if (scope.seesAll()) { - return scope; - } - if (execution != null && Objects.equals(execution.getOwnerId(), userId)) { - return VisibilityScope.all(); - } - return scope; + validateExecutionExists(projectId, executionId); + // 进得来 = 看执行下全部任务,"我参与 / 所有"由前端发不发 reqVO.ownerId / involveUserId 决定。 + // 注:getTaskRespVOPage 内部 this.getTaskPage() 自调用不触发 AOP,但外层注解已守门; + // 此处独立挂注解是为了堵跨 service 直调 ProjectTaskService.getTaskPage 的鉴权后门。 + return projectTaskMapper.selectPageByExecutionId(projectId, executionId, reqVO); } @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) public ProjectTaskRespVO getTaskRespVO(Long projectId, Long executionId, Long taskId) { // 内联 validate,便于接住 execution 供前端 executionOwnerId 字段使用 ProjectExecutionDO execution = validateExecutionExists(projectId, executionId); ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId); - // 可见性卡断:执行 owner / 项目经理直接放行;否则 taskId 必须在 scope.taskIds 中。 - // 未命中按"任务不存在"语义返回,不暴露存在性。 - Long userId = SecurityFrameworkUtils.getLoginUserId(); - if (!Objects.equals(execution.getOwnerId(), userId)) { - VisibilityScope scope = visibilityScopeResolver.resolveForExecution(projectId, executionId, userId); - if (!scope.seesAll() && !scope.taskIds().contains(taskId)) { - throw exception(ErrorCodeConstants.PROJECT_TASK_NOT_EXISTS); - } - } ProjectTaskRespVO respVO = BeanUtils.toBean(task, ProjectTaskRespVO.class); applyLifecycle(respVO); respVO.setOwnerNickname(loadOwnerNickname(task.getOwnerId())); @@ -488,6 +463,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { } @Override + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectTaskConstants.PERMISSION_QUERY) public PageResult getTaskRespVOPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) { PageResult pageResult = getTaskPage(projectId, executionId, reqVO); return assembleTaskRespVOPage(projectId, executionId, pageResult); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/util/DueRangeSupport.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/util/DueRangeSupport.java new file mode 100644 index 0000000..21907d8 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/util/DueRangeSupport.java @@ -0,0 +1,38 @@ +package com.njcn.rdms.module.project.util; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; + +/** + * dueRange 截止时间范围筛选的日期边界 helper。 + * + *

口径统一:服务器时区 {@code Asia/Shanghai},本周按周一~周日。 + * 供执行分页查询与执行状态看板计数共用,避免在多个 service 里重复同一段日期计算。

+ * + *

终态排除不在此处:终态码由各对象域自行通过 + * {@code ObjectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(objectType)} 动态查。

+ */ +public final class DueRangeSupport { + + private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Shanghai"); + + private DueRangeSupport() { + } + + /** 服务器当天。 */ + public static LocalDate today() { + return LocalDate.now(SERVER_ZONE); + } + + /** 本周一(含当天)。 */ + public static LocalDate weekStart(LocalDate today) { + return today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + } + + /** 本周日(含当天)。 */ + public static LocalDate weekEnd(LocalDate today) { + return today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml b/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml index 831ac25..f89b278 100644 --- a/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml +++ b/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml @@ -56,7 +56,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: njcnpqs diff --git a/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml b/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml index dff2ac4..4135ce4 100644 --- a/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml +++ b/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml @@ -55,7 +55,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: njcnpqs diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceTest.java index 78b5f0a..dca9cd4 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectStatusBoardServiceTest.java @@ -9,9 +9,6 @@ import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper; 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.service.project.permission.VisibilityScope; -import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -24,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest { @@ -37,17 +33,6 @@ class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest { private ProjectExecutionMapper projectExecutionMapper; @Mock private ProjectTaskMapper projectTaskMapper; - @Mock - private VisibilityScopeResolver visibilityScopeResolver; - - /** - * 默认让 VisibilityScopeResolver 放行(seesAll=true),既有看板用例不关心 scope。 - */ - @BeforeEach - void setupVisibilityScopeAll() { - lenient().when(visibilityScopeResolver.resolveForProject(any(), any())).thenReturn(VisibilityScope.all()); - lenient().when(visibilityScopeResolver.resolveForExecution(any(), any(), any())).thenReturn(VisibilityScope.all()); - } @Test void getExecutionStatusBoard_shouldReturnEnabledStatusesInSortOrderAndSumCounts() { @@ -59,16 +44,21 @@ class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest { createStatus("cancelled", "已取消", 50, true), createStatus("disabled", "已停用", 60, false, 1) )); - when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class), - any(ProjectExecutionStatusBoardReqVO.class), eq("pending"))).thenReturn(3); - when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class), - any(ProjectExecutionStatusBoardReqVO.class), eq("active"))).thenReturn(8); - when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class), - any(ProjectExecutionStatusBoardReqVO.class), eq("paused"))).thenReturn(2); - when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class), - any(ProjectExecutionStatusBoardReqVO.class), eq("completed"))).thenReturn(4); - when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class), - any(ProjectExecutionStatusBoardReqVO.class), eq("cancelled"))).thenReturn(1); + when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), + any(ProjectExecutionStatusBoardReqVO.class), eq("pending"), + any(), any(), any(), any())).thenReturn(3); + when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), + any(ProjectExecutionStatusBoardReqVO.class), eq("active"), + any(), any(), any(), any())).thenReturn(8); + when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), + any(ProjectExecutionStatusBoardReqVO.class), eq("paused"), + any(), any(), any(), any())).thenReturn(2); + when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), + any(ProjectExecutionStatusBoardReqVO.class), eq("completed"), + any(), any(), any(), any())).thenReturn(4); + when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), + any(ProjectExecutionStatusBoardReqVO.class), eq("cancelled"), + any(), any(), any(), any())).thenReturn(1); ProjectExecutionStatusBoardReqVO reqVO = new ProjectExecutionStatusBoardReqVO(); reqVO.setKeyword("接口"); @@ -100,15 +90,15 @@ class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest { createStatus("cancelled", "已取消", 50, true) )); when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L), - any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("pending"))).thenReturn(5); + any(ProjectTaskStatusBoardReqVO.class), eq("pending"))).thenReturn(5); when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L), - any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("active"))).thenReturn(12); + any(ProjectTaskStatusBoardReqVO.class), eq("active"))).thenReturn(12); when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L), - any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("paused"))).thenReturn(2); + any(ProjectTaskStatusBoardReqVO.class), eq("paused"))).thenReturn(2); when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L), - any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("completed"))).thenReturn(4); + any(ProjectTaskStatusBoardReqVO.class), eq("completed"))).thenReturn(4); when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L), - any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("cancelled"))).thenReturn(1); + any(ProjectTaskStatusBoardReqVO.class), eq("cancelled"))).thenReturn(1); ProjectTaskStatusBoardReqVO reqVO = new ProjectTaskStatusBoardReqVO(); reqVO.setKeyword("任务"); diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImplTest.java index 89bbe58..3efc4e9 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImplTest.java @@ -98,23 +98,16 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest { @Mock private ProjectExecutionAssigneeService projectExecutionAssigneeService; @Mock - private com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver visibilityScopeResolver; - @Mock private ProjectRequirementService projectRequirementService; @Mock private ProjectRequirementMapper projectRequirementMapper; /** - * 默认让 VisibilityScopeResolver 放行(seesAll=true),既有测试无需关心 scope。 * 默认让 dictDataApi.validateDictDataList 对 REQ_PRIORITY 返回 true,既有测试不因 priority 校验失败。 - * 真正需要测试 scope 行为的用例可在方法内显式覆盖。 + * 读路径鉴权由 @CheckObjectPermission 的 AOP 处理,单测 @InjectMocks 不走 AOP,无须在此 mock。 */ @BeforeEach - void setupVisibilityScopeAll() { - lenient().when(visibilityScopeResolver.resolveForProject(any(), any())) - .thenReturn(com.njcn.rdms.module.project.service.project.permission.VisibilityScope.all()); - lenient().when(visibilityScopeResolver.resolveForExecution(any(), any(), any())) - .thenReturn(com.njcn.rdms.module.project.service.project.permission.VisibilityScope.all()); + void setupDefaultPriorityValidation() { lenient().when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any())) .thenReturn(success(true)); } @@ -557,7 +550,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest { second.setProgressRate(new BigDecimal("100.00")); when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId)); - when(projectExecutionMapper.selectPageByProjectId(eq(projectId), any(), eq(reqVO))) + when(projectExecutionMapper.selectPageByProjectId(eq(projectId), eq(reqVO), any(), any(), any(), any())) .thenReturn(new PageResult<>(List.of(first, second), 2L)); when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task")) .thenReturn(List.of("cancelled")); @@ -593,7 +586,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest { rows.add(Map.of("executionId", 5002L, "progressRate", 10)); when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId)); - when(projectExecutionMapper.selectPageByProjectId(eq(projectId), any(), eq(reqVO))) + when(projectExecutionMapper.selectPageByProjectId(eq(projectId), eq(reqVO), any(), any(), any(), any())) .thenReturn(new PageResult<>(List.of(first, second), 2L)); when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task")) .thenReturn(List.of("cancelled")); @@ -654,7 +647,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest { reqVO.setPageNo(1); reqVO.setPageSize(20); when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId)); - when(projectExecutionMapper.selectPageByProjectId(eq(projectId), any(), eq(reqVO))) + when(projectExecutionMapper.selectPageByProjectId(eq(projectId), eq(reqVO), any(), any(), any(), any())) .thenReturn(new PageResult<>(List.of(), 0L)); PageResult result = projectExecutionService.getExecutionPage(projectId, reqVO); diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolverImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolverImplTest.java deleted file mode 100644 index efa8722..0000000 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolverImplTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.njcn.rdms.module.project.service.project.permission; - -import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest; -import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; -import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; -import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper; -import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper; -import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper; -import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import java.util.List; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; - -/** - * VisibilityScopeResolverImpl 单元测试。覆盖角色矩阵: - * - 项目经理 → seesAll - * - 非项目经理 → 4 源并集 - * - 非项目经理且无任何参与 → 空集合 - * - 执行维度同上 - */ -class VisibilityScopeResolverImplTest extends BaseMockitoUnitTest { - - @InjectMocks - private VisibilityScopeResolverImpl resolver; - - @Mock private ProjectMapper projectMapper; - @Mock private ProjectExecutionMapper projectExecutionMapper; - @Mock private ExecutionAssigneeMapper executionAssigneeMapper; - @Mock private ProjectTaskMapper projectTaskMapper; - @Mock private TaskAssigneeMapper taskAssigneeMapper; - - @Test - void resolveForProject_managerShouldSeeAll() { - Long projectId = 2001L, userId = 3001L; - ProjectDO project = new ProjectDO(); - project.setId(projectId); - project.setManagerUserId(userId); - when(projectMapper.selectById(projectId)).thenReturn(project); - - VisibilityScope scope = resolver.resolveForProject(projectId, userId); - - assertTrue(scope.seesAll()); - } - - @Test - void resolveForProject_nonManagerUnionsFourSources() { - Long projectId = 2001L, userId = 3002L; - ProjectDO project = new ProjectDO(); - project.setId(projectId); - project.setManagerUserId(9999L); - when(projectMapper.selectById(projectId)).thenReturn(project); - - when(projectExecutionMapper.selectIdsByProjectIdAndOwnerId(projectId, userId)) - .thenReturn(List.of(5001L)); - when(executionAssigneeMapper.selectActiveExecutionIdsByProjectIdAndUserId(projectId, userId)) - .thenReturn(List.of(5002L)); - when(projectTaskMapper.selectOwnedTaskAndDescendantIdsByProjectIdAndUserId(projectId, userId)) - .thenReturn(List.of(9001L, 9002L)); - when(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndUserId(projectId, userId)) - .thenReturn(List.of(9003L)); - - VisibilityScope scope = resolver.resolveForProject(projectId, userId); - - assertFalse(scope.seesAll()); - assertEquals(Set.of(5001L, 5002L), scope.executionIds()); - assertEquals(Set.of(9001L, 9002L, 9003L), scope.taskIds()); - } - - @Test - void resolveForProject_nonParticipantReturnsEmpty() { - Long projectId = 2001L, userId = 3099L; - ProjectDO project = new ProjectDO(); - project.setId(projectId); - project.setManagerUserId(9999L); - when(projectMapper.selectById(projectId)).thenReturn(project); - when(projectExecutionMapper.selectIdsByProjectIdAndOwnerId(projectId, userId)).thenReturn(List.of()); - when(executionAssigneeMapper.selectActiveExecutionIdsByProjectIdAndUserId(projectId, userId)).thenReturn(List.of()); - when(projectTaskMapper.selectOwnedTaskAndDescendantIdsByProjectIdAndUserId(projectId, userId)).thenReturn(List.of()); - when(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndUserId(projectId, userId)).thenReturn(List.of()); - - VisibilityScope scope = resolver.resolveForProject(projectId, userId); - - assertFalse(scope.seesAll()); - assertTrue(scope.executionIds().isEmpty()); - assertTrue(scope.taskIds().isEmpty()); - } - - @Test - void resolveForExecution_managerShouldSeeAll() { - Long projectId = 2001L, executionId = 5001L, userId = 3001L; - ProjectDO project = new ProjectDO(); - project.setManagerUserId(userId); - when(projectMapper.selectById(projectId)).thenReturn(project); - - VisibilityScope scope = resolver.resolveForExecution(projectId, executionId, userId); - - assertTrue(scope.seesAll()); - } - - @Test - void resolveForExecution_nonManagerScopedToThatExecution() { - Long projectId = 2001L, executionId = 5001L, userId = 3002L; - ProjectDO project = new ProjectDO(); - project.setManagerUserId(9999L); - when(projectMapper.selectById(projectId)).thenReturn(project); - - when(projectTaskMapper.selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId(projectId, executionId, userId)) - .thenReturn(List.of(9001L)); - when(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndExecutionIdAndUserId(projectId, executionId, userId)) - .thenReturn(List.of(9002L)); - - VisibilityScope scope = resolver.resolveForExecution(projectId, executionId, userId); - - assertFalse(scope.seesAll()); - assertTrue(scope.executionIds().isEmpty()); - assertEquals(Set.of(9001L, 9002L), scope.taskIds()); - } -} diff --git a/rdms-system/rdms-system-boot/src/main/resources/application-dev.yaml b/rdms-system/rdms-system-boot/src/main/resources/application-dev.yaml index 79c21f7..6322d31 100644 --- a/rdms-system/rdms-system-boot/src/main/resources/application-dev.yaml +++ b/rdms-system/rdms-system-boot/src/main/resources/application-dev.yaml @@ -56,7 +56,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: njcnpqs # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 diff --git a/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml b/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml index 42119fb..81d8577 100644 --- a/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml +++ b/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml @@ -55,7 +55,7 @@ spring: primary: master datasource: master: - url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 username: root password: njcnpqs