docs: 删除工单需求规格文档并更新开发规范
- 删除了工单需求规格说明文档 2026-05-22-ticket-design.md - 在安全注解 CheckObjectPermission 中新增 accessible 参数配置 - 更新 CLAUDE.md 开发规范文档,补充 MySQL 客户端使用说明 - 优化错误码常量中的错误消息格式,使用中文状态和操作名称 - 修复权限拒绝提示消息,提供更友好的用户提示 - 更新开发规范关于演示库同步补丁和文档输出格式的要求
This commit is contained in:
@@ -14,8 +14,8 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PRODUCT_CODE_DUPLICATE = new ErrorCode(1_008_001_001, "已经存在编码为【{}】的产品");
|
||||
ErrorCode PRODUCT_NAME_DUPLICATE = new ErrorCode(1_008_001_002, "已经存在名称为【{}】的产品");
|
||||
ErrorCode PRODUCT_CODE_NOT_MODIFIABLE = new ErrorCode(1_008_001_003, "产品编码创建后不允许修改");
|
||||
ErrorCode PRODUCT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_001_004, "当前产品状态不支持动作【{}】");
|
||||
ErrorCode PRODUCT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_001_005, "动作【{}】必须填写原因");
|
||||
ErrorCode PRODUCT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_001_004, "当前产品为「{}」状态,不支持「{}」操作");
|
||||
ErrorCode PRODUCT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_001_005, "「{}」操作必须填写原因");
|
||||
ErrorCode PRODUCT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_001_006, "删除确认名称与当前产品名称不一致");
|
||||
ErrorCode PRODUCT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_001_007, "当前产品状态不允许编辑");
|
||||
ErrorCode PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE = new ErrorCode(1_008_001_008, "产品暂停后仅允许修正描述,产品经理请通过产品团队维护");
|
||||
@@ -29,7 +29,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PRODUCT_MANAGER_TRANSFER_SOURCE_INVALID = new ErrorCode(1_008_001_017, "原产品经理信息与当前产品经理不一致");
|
||||
ErrorCode PRODUCT_MANAGER_TRANSFER_ROLE_INVALID = new ErrorCode(1_008_001_018, "原产品经理交接后的角色不能仍为产品经理");
|
||||
ErrorCode PRODUCT_MANAGER_NOT_MODIFIABLE = new ErrorCode(1_008_001_019, "产品主数据编辑不允许直接变更产品经理,请通过产品团队维护");
|
||||
ErrorCode PRODUCT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_001_020, "当前用户不具备该产品的操作权限【{}】");
|
||||
ErrorCode PRODUCT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_001_020, "您没有该产品的此项操作权限,请联系管理员");
|
||||
ErrorCode PRODUCT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_001_021, "删除确认口令不正确");
|
||||
ErrorCode PRODUCT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_001_022, "产品状态已发生变化,请刷新后重试");
|
||||
ErrorCode PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_001_023, "产品状态定义不存在或已停用");
|
||||
@@ -38,7 +38,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PRODUCT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_001_025, "初始团队成员存在重复");
|
||||
ErrorCode PRODUCT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_001_026, "初始团队中存在非法角色");
|
||||
ErrorCode PRODUCT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_001_027, "原产品经理在该产品已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试");
|
||||
ErrorCode PRODUCT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_001_028, "内置产品角色【{}】未在 system_role 找到,请联系管理员");
|
||||
ErrorCode PRODUCT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_001_028, "内置产品角色【{}】未配置,请联系管理员");
|
||||
ErrorCode PRODUCT_MEMBER_USER_INVALID = new ErrorCode(1_008_001_029, "产品成员不是有效系统用户");
|
||||
// 批量新增(POST /project/product/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
||||
ErrorCode PRODUCT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_001_030, "请勿在批量列表中重复添加同一成员");
|
||||
@@ -48,8 +48,8 @@ public interface ErrorCodeConstants {
|
||||
|
||||
// ========== 产品需求 1-008-002-000 ==========
|
||||
ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_001, "当前需求状态不支持动作【{}】");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_002, "动作【{}】必须填写原因");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_001, "当前需求为「{}」状态,不支持「{}」操作");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_002, "「{}」操作必须填写原因");
|
||||
ErrorCode REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_003, "需求状态已发生变化,请刷新后重试");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_004, "当前需求状态不允许编辑");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_005, "只有已验收的需求才能关闭");
|
||||
@@ -85,8 +85,8 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROJECT_TYPE_INVALID = new ErrorCode(1_008_002_006, "项目类型不是有效字典值");
|
||||
ErrorCode PROJECT_MANAGER_USER_INVALID = new ErrorCode(1_008_002_007, "项目负责人不是有效系统用户");
|
||||
ErrorCode PROJECT_MEMBER_USER_INVALID = new ErrorCode(1_008_002_008, "项目成员不是有效系统用户");
|
||||
ErrorCode PROJECT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_009, "当前项目状态不支持动作【{}】");
|
||||
ErrorCode PROJECT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_010, "动作【{}】必须填写原因");
|
||||
ErrorCode PROJECT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_009, "当前项目为「{}」状态,不支持「{}」操作");
|
||||
ErrorCode PROJECT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_010, "「{}」操作必须填写原因");
|
||||
ErrorCode PROJECT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_011, "项目状态已发生变化,请刷新后重试");
|
||||
ErrorCode PROJECT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_012, "当前项目状态不允许编辑");
|
||||
ErrorCode PROJECT_MEMBER_NOT_EXISTS = new ErrorCode(1_008_002_013, "项目成员不存在");
|
||||
@@ -97,7 +97,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROJECT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_002_018, "删除确认口令不正确");
|
||||
ErrorCode PROJECT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_002_019, "删除确认名称与当前项目名称不一致");
|
||||
ErrorCode PROJECT_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_020, "当前项目不允许删除");
|
||||
ErrorCode PROJECT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_002_021, "当前用户不具备该项目的操作权限【{}】");
|
||||
ErrorCode PROJECT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_002_021, "您没有该项目的此项操作权限,请联系管理员");
|
||||
ErrorCode PROJECT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_022, "项目状态定义不存在或已停用");
|
||||
ErrorCode PROJECT_DIRECTION_INVALID = new ErrorCode(1_008_002_023, "项目方向不是有效字典值");
|
||||
ErrorCode PROJECT_MANAGER_TRANSFER_INFO_REQUIRED = new ErrorCode(1_008_002_024, "切换项目经理时必须同时传入原项目经理用户和交接后角色");
|
||||
@@ -111,7 +111,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROJECT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_002_031, "初始团队中存在非法角色");
|
||||
ErrorCode PROJECT_DIRECTION_NOT_MATCH_PRODUCT = new ErrorCode(1_008_002_032, "项目方向与所属产品方向不一致");
|
||||
ErrorCode PROJECT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_002_033, "原项目经理在该项目已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试");
|
||||
ErrorCode PROJECT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_002_034, "内置项目角色【{}】未在 system_role 找到,请联系管理员");
|
||||
ErrorCode PROJECT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_002_034, "内置项目角色【{}】未配置,请联系管理员");
|
||||
// 批量新增(POST /project/project/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
||||
ErrorCode PROJECT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_002_035, "请勿在批量列表中重复添加同一成员");
|
||||
ErrorCode PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_002_036, "批量新增不允许指定为经理,请通过编辑成员调整");
|
||||
@@ -131,13 +131,13 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_008, "当前项目状态不允许维护执行");
|
||||
ErrorCode PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED = new ErrorCode(1_008_003_009, "该项目成员仍担任未终态执行负责人,请先完成执行负责人交接");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_003_010, "执行状态定义不存在或已停用");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_003_011, "当前执行状态不支持动作【{}】");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_003_012, "动作【{}】必须填写原因");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_003_011, "当前执行为「{}」状态,不支持「{}」操作");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_003_012, "「{}」操作必须填写原因");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_003_013, "执行状态已发生变化,请刷新后重试");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_014, "当前执行状态不允许维护执行");
|
||||
ErrorCode PROJECT_EXECUTION_TYPE_INVALID = new ErrorCode(1_008_003_015, "执行类型不是有效字典值");
|
||||
ErrorCode PROJECT_EXECUTION_ASSIGNEE_REQUIRED = new ErrorCode(1_008_003_016, "创建执行时必须至少选择一名执行协办人");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_OWNER_ONLY = new ErrorCode(1_008_003_017, "只有执行负责人才能执行【{}】动作");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_OWNER_ONLY = new ErrorCode(1_008_003_017, "只有执行负责人才能执行「{}」操作");
|
||||
ErrorCode PROJECT_EXECUTION_COMPLETE_TASKS_REQUIRED = new ErrorCode(1_008_003_018, "完成执行前,执行下所有任务必须全部完成或取消");
|
||||
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_DELETE = new ErrorCode(1_008_003_019, "已完成的执行不允许删除");
|
||||
ErrorCode PROJECT_EXECUTION_DELETE_NAME_MISMATCH = new ErrorCode(1_008_003_020, "确认执行名称与实际不一致");
|
||||
@@ -153,12 +153,12 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROJECT_TASK_PARENT_INVALID = new ErrorCode(1_008_004_002, "父任务必须属于当前项目和执行");
|
||||
ErrorCode PROJECT_TASK_NOT_ALLOW_EDIT = new ErrorCode(1_008_004_003, "当前项目或执行状态不允许维护任务");
|
||||
ErrorCode PROJECT_TASK_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_004_004, "任务状态定义不存在或已停用");
|
||||
ErrorCode PROJECT_TASK_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_004_005, "当前任务状态不支持动作【{}】");
|
||||
ErrorCode PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_004_006, "动作【{}】必须填写原因");
|
||||
ErrorCode PROJECT_TASK_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_004_005, "当前任务为「{}」状态,不支持「{}」操作");
|
||||
ErrorCode PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_004_006, "「{}」操作必须填写原因");
|
||||
ErrorCode PROJECT_TASK_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_004_007, "任务状态已发生变化,请刷新后重试");
|
||||
ErrorCode PROJECT_TASK_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_004_008, "当前任务状态不允许维护任务");
|
||||
ErrorCode PROJECT_TASK_COMPLETE_CHILDREN_REQUIRED = new ErrorCode(1_008_004_010, "父任务完成前,子任务必须全部完成或取消");
|
||||
ErrorCode PROJECT_TASK_STATUS_OWNER_ONLY = new ErrorCode(1_008_004_011, "只有任务负责人才能执行【{}】动作");
|
||||
ErrorCode PROJECT_TASK_STATUS_OWNER_ONLY = new ErrorCode(1_008_004_011, "只有任务负责人才能执行「{}」操作");
|
||||
ErrorCode PROJECT_TASK_NOT_ALLOW_DELETE = new ErrorCode(1_008_004_012, "已完成的任务不允许删除");
|
||||
ErrorCode PROJECT_TASK_DELETE_NAME_MISMATCH = new ErrorCode(1_008_004_013, "确认任务名称与实际不一致");
|
||||
ErrorCode PROJECT_TASK_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_004_014, "删除确认口令必须为 DELETE 或 删除");
|
||||
@@ -195,8 +195,8 @@ public interface ErrorCodeConstants {
|
||||
|
||||
// ========== 项目需求 1_008_007_xxx ==========
|
||||
ErrorCode PROJECT_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "项目需求不存在");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_007_001, "当前项目需求状态不支持动作【{}】");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_007_002, "动作【{}】必须填写原因");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_007_001, "当前项目需求为「{}」状态,不支持「{}」操作");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_007_002, "「{}」操作必须填写原因");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_007_003, "项目需求状态已发生变化,请刷新后重试");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_007_004, "当前项目需求状态不允许编辑");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_005, "只有已验收的项目需求才能关闭");
|
||||
@@ -221,8 +221,8 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PERSONAL_ITEM_NOT_EXISTS = new ErrorCode(1_008_008_001, "个人事项不存在");
|
||||
ErrorCode PERSONAL_ITEM_OWNER_NOT_IN_EXECUTION = new ErrorCode(1_008_008_002, "个人事项负责人必须属于当前有效执行团队成员");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_008_003, "个人事项状态定义不存在或已停用");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_008_004, "当前个人事项状态不支持动作【{}】");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_008_005, "动作【{}】必须填写原因");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_008_004, "当前个人事项为「{}」状态,不支持「{}」操作");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_008_005, "「{}」操作必须填写原因");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_008_006, "个人事项状态已发生变化,请刷新后重试");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_008_007, "当前个人事项状态不允许编辑");
|
||||
ErrorCode PERSONAL_ITEM_NOT_ALLOW_DELETE = new ErrorCode(1_008_008_008, "仅初始态(待开始)的个人事项允许删除");
|
||||
@@ -231,8 +231,8 @@ public interface ErrorCodeConstants {
|
||||
// ========== 加班申请 1_008_009_xxx ==========
|
||||
ErrorCode OVERTIME_APPLICATION_NOT_EXISTS = new ErrorCode(1_008_009_001, "加班申请不存在");
|
||||
ErrorCode OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_009_002, "加班申请状态定义不存在或已停用");
|
||||
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_009_003, "当前加班申请状态不支持动作【{}】");
|
||||
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_009_004, "动作【{}】必须填写原因");
|
||||
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_009_003, "当前加班申请为「{}」状态,不支持「{}」操作");
|
||||
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_009_004, "「{}」操作必须填写原因");
|
||||
ErrorCode OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_009_005, "加班申请状态已发生变化,请刷新后重试");
|
||||
ErrorCode OVERTIME_APPLICATION_APPLICANT_ONLY = new ErrorCode(1_008_009_006, "仅申请人可执行该操作");
|
||||
ErrorCode OVERTIME_APPLICATION_APPROVER_ONLY = new ErrorCode(1_008_009_007, "仅当前审核人可执行该操作");
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionRespVO;
|
||||
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 我负责的执行(跨项目)")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/me/executions")
|
||||
@Validated
|
||||
public class MyExecutionController {
|
||||
|
||||
@Resource
|
||||
private ProjectExecutionService projectExecutionService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页获取当前登录用户负责的执行(跨项目,默认排除终态与进度满)")
|
||||
public CommonResult<PageResult<MyProjectExecutionRespVO>> getMyExecutionPage(@Valid MyProjectExecutionPageReqVO reqVO) {
|
||||
// 前端固定传 pageSize=-1 拉全部;负数统一归一为 PAGE_SIZE_NONE(-1),与现有执行分页接口一致
|
||||
if (reqVO.getPageSize() != null && reqVO.getPageSize() < 0) {
|
||||
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
}
|
||||
return success(projectExecutionService.getMyExecutionPage(reqVO));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Schema(description = "管理后台 - 我负责的执行(跨项目)分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MyProjectExecutionPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "执行状态编码(预留,单状态精确过滤)", example = "active")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "执行名称模糊匹配关键字(预留)", example = "联调")
|
||||
private String keyword;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Schema(description = "管理后台 - 我负责的执行(跨项目)Response VO")
|
||||
@Data
|
||||
public class MyProjectExecutionRespVO {
|
||||
|
||||
@Schema(description = "执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
||||
private Long id;
|
||||
@Schema(description = "执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调")
|
||||
private String executionName;
|
||||
@Schema(description = "所属项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001")
|
||||
private Long projectId;
|
||||
@Schema(description = "所属项目名称", example = "商城 V2 升级")
|
||||
private String projectName;
|
||||
@Schema(description = "执行状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||
private String statusCode;
|
||||
@Schema(description = "执行状态名称", example = "进行中")
|
||||
private String statusName;
|
||||
@Schema(description = "优先级编码(字典 rdms_req_priority),0=P0(最高) ~ 3=P3(最低)", example = "0")
|
||||
private String priority;
|
||||
@Schema(description = "计划开始日期")
|
||||
private LocalDate plannedStartDate;
|
||||
@Schema(description = "计划结束日期")
|
||||
private LocalDate plannedEndDate;
|
||||
@Schema(description = "实际开始日期")
|
||||
private LocalDate actualStartDate;
|
||||
@Schema(description = "实际结束日期")
|
||||
private LocalDate actualEndDate;
|
||||
@Schema(description = "执行进度百分比 0-100", example = "68")
|
||||
private Integer progressRate;
|
||||
@Schema(description = "关联项目需求编号")
|
||||
private Long projectRequirementId;
|
||||
@Schema(description = "关联项目需求名称", example = "订单履约后端拆分(一期)")
|
||||
private String projectRequirementName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.project;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectOwnedRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectParticipatedRespVO;
|
||||
import com.njcn.rdms.module.project.service.project.MyProjectService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 工作台「我的项目」")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/me")
|
||||
@Validated
|
||||
public class MyProjectController {
|
||||
|
||||
@Resource
|
||||
private MyProjectService myProjectService;
|
||||
|
||||
@GetMapping("/participated/page")
|
||||
@Operation(summary = "分页获取当前登录用户参与的项目(作为成员)")
|
||||
public CommonResult<PageResult<MyProjectParticipatedRespVO>> getMyParticipatedPage(@Valid MyProjectPageReqVO reqVO) {
|
||||
normalizePageSize(reqVO);
|
||||
return success(myProjectService.getMyParticipatedPage(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/owned/page")
|
||||
@Operation(summary = "分页获取当前登录用户负责的项目(managerUserId=当前用户)")
|
||||
public CommonResult<PageResult<MyProjectOwnedRespVO>> getMyOwnedPage(@Valid MyProjectPageReqVO reqVO) {
|
||||
normalizePageSize(reqVO);
|
||||
return success(myProjectService.getMyOwnedPage(reqVO));
|
||||
}
|
||||
|
||||
/** 前端固定传 pageSize=-1 拉全部;负数统一归一为 PAGE_SIZE_NONE,与 MyExecutionController 一致。 */
|
||||
private void normalizePageSize(MyProjectPageReqVO reqVO) {
|
||||
if (reqVO.getPageSize() != null && reqVO.getPageSize() < 0) {
|
||||
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 我负责的项目 Response VO")
|
||||
@Data
|
||||
public class MyProjectOwnedRespVO {
|
||||
|
||||
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "商城 V2 升级")
|
||||
private String name;
|
||||
@Schema(description = "项目编码", example = "MALL-V2")
|
||||
private String code;
|
||||
@Schema(description = "项目整体进度百分比 0-100", requiredMode = Schema.RequiredMode.REQUIRED, example = "70")
|
||||
private Integer progress;
|
||||
@Schema(description = "当前用户在该项目中的角色名(恒含负责人语义)", example = "项目负责人")
|
||||
private String myRole;
|
||||
@Schema(description = "项目计划结束日期 YYYY-MM-DD;未设为 null")
|
||||
private LocalDate plannedEndDate;
|
||||
@Schema(description = "项目下进行中执行数", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
|
||||
private Integer executionCount;
|
||||
@Schema(description = "项目下进行中任务数", requiredMode = Schema.RequiredMode.REQUIRED, example = "24")
|
||||
private Integer taskCount;
|
||||
@Schema(description = "项目当前有效成员数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||
private Integer memberCount;
|
||||
@Schema(description = "项目下逾期任务数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
private Integer overdueCount;
|
||||
@Schema(description = "成员负载原始数据;无成员为 []", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<MemberLoadVO> members;
|
||||
|
||||
@Schema(description = "成员负载原始数据")
|
||||
@Data
|
||||
public static class MemberLoadVO {
|
||||
@Schema(description = "成员用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "101")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
@Schema(description = "成员姓名/昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
|
||||
private String userName;
|
||||
@Schema(description = "该成员在本项目下的进行中任务数", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
|
||||
private Integer activeTaskCount;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Schema(description = "管理后台 - 工作台「我的项目」分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MyProjectPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "项目名称/编码模糊匹配关键字(预留,本期不过滤)", example = "商城")
|
||||
private String keyword;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 我参与的项目 Response VO")
|
||||
@Data
|
||||
public class MyProjectParticipatedRespVO {
|
||||
|
||||
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "商城 V2 升级")
|
||||
private String name;
|
||||
@Schema(description = "项目编码", example = "MALL-V2")
|
||||
private String code;
|
||||
@Schema(description = "项目状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||
private String statusCode;
|
||||
@Schema(description = "项目状态名称", example = "进行中")
|
||||
private String statusName;
|
||||
@Schema(description = "项目整体进度百分比 0-100", requiredMode = Schema.RequiredMode.REQUIRED, example = "70")
|
||||
private Integer progress;
|
||||
@Schema(description = "当前用户在该项目中的角色名(主角色 / 附加角色拼接)", example = "前端负责人")
|
||||
private String myRole;
|
||||
@Schema(description = "我负责的任务总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8")
|
||||
private Integer myTaskCount;
|
||||
@Schema(description = "我负责的未完成任务数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
private Integer myPendingTaskCount;
|
||||
|
||||
}
|
||||
@@ -111,4 +111,18 @@ public interface UserObjectRoleMapper extends BaseMapperX<UserObjectRoleDO> {
|
||||
.eq(UserObjectRoleDO::getStatus, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作台「我负责的项目」:批量查一批对象下的活跃成员角色行(status=0)。
|
||||
* 一次拿全,内存按 objectId 分组,避免逐项目 N+1。
|
||||
*/
|
||||
default List<UserObjectRoleDO> selectActiveListByObjectTypeAndObjectIds(String objectType, Collection<Long> objectIds) {
|
||||
if (objectIds == null || objectIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return selectList(new LambdaQueryWrapperX<UserObjectRoleDO>()
|
||||
.eq(UserObjectRoleDO::getObjectType, objectType)
|
||||
.in(UserObjectRoleDO::getObjectId, objectIds)
|
||||
.eq(UserObjectRoleDO::getStatus, 0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -277,4 +277,27 @@ public interface ProjectExecutionMapper extends BaseMapperX<ProjectExecutionDO>
|
||||
.eq(ProjectExecutionDO::getStatusCode, fromStatus));
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口二:一批项目下的进行中执行数(按 project_id 分组,排除终态)。
|
||||
* 返回 Map:projectId(Long) / executionCount(Long)。
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT project_id AS projectId,
|
||||
CAST(COUNT(*) AS SIGNED) AS executionCount
|
||||
FROM rdms_project_execution
|
||||
WHERE deleted = b'0'
|
||||
AND project_id IN
|
||||
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
|
||||
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||
AND status_code NOT IN
|
||||
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||
</if>
|
||||
GROUP BY project_id
|
||||
</script>
|
||||
""")
|
||||
List<Map<String, Object>> selectExecutionCountGroupByProjectIds(
|
||||
@Param("projectIds") Collection<Long> projectIds,
|
||||
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes);
|
||||
|
||||
}
|
||||
|
||||
@@ -692,4 +692,91 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
||||
private Long count;
|
||||
}
|
||||
|
||||
// ======================== 工作台「我的项目」聚合计数 ========================
|
||||
|
||||
/**
|
||||
* 接口一:当前用户(owner_id)在一批项目下的任务总数与未完成数(按 project_id 分组)。
|
||||
* totalCount=全部我负责任务;pendingCount=状态非终态(终态集为空则等于 totalCount)。
|
||||
* 返回 Map:projectId(Long) / totalCount(Long) / pendingCount(Long)。
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT project_id AS projectId,
|
||||
CAST(COUNT(*) AS SIGNED) AS totalCount,
|
||||
CAST(SUM(CASE WHEN 1 = 1
|
||||
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||
AND status_code NOT IN
|
||||
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||
</if>
|
||||
THEN 1 ELSE 0 END) AS SIGNED) AS pendingCount
|
||||
FROM rdms_task
|
||||
WHERE deleted = b'0'
|
||||
AND owner_id = #{ownerId}
|
||||
AND project_id IN
|
||||
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
|
||||
GROUP BY project_id
|
||||
</script>
|
||||
""")
|
||||
List<Map<String, Object>> selectMyTaskCountGroupByProjectIds(
|
||||
@Param("ownerId") Long ownerId,
|
||||
@Param("projectIds") Collection<Long> projectIds,
|
||||
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes);
|
||||
|
||||
/**
|
||||
* 接口二:一批项目下的进行中任务数与逾期任务数(按 project_id 分组,一次扫表出两数)。
|
||||
* taskCount=状态非终态;overdueCount=planned_end_date < today 且状态非终态。
|
||||
* 返回 Map:projectId(Long) / taskCount(Long) / overdueCount(Long)。
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT project_id AS projectId,
|
||||
CAST(SUM(CASE WHEN 1 = 1
|
||||
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||
AND status_code NOT IN
|
||||
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||
</if>
|
||||
THEN 1 ELSE 0 END) AS SIGNED) AS taskCount,
|
||||
CAST(SUM(CASE WHEN planned_end_date < #{today}
|
||||
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||
AND status_code NOT IN
|
||||
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||
</if>
|
||||
THEN 1 ELSE 0 END) AS SIGNED) AS overdueCount
|
||||
FROM rdms_task
|
||||
WHERE deleted = b'0'
|
||||
AND project_id IN
|
||||
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
|
||||
GROUP BY project_id
|
||||
</script>
|
||||
""")
|
||||
List<Map<String, Object>> selectTaskAndOverdueCountGroupByProjectIds(
|
||||
@Param("projectIds") Collection<Long> projectIds,
|
||||
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||
@Param("today") LocalDate today);
|
||||
|
||||
/**
|
||||
* 接口二 members:一批项目下每个负责人(owner_id)的进行中任务数(按 project_id, owner_id 分组)。
|
||||
* 排除 owner_id 为空的任务。返回 Map:projectId(Long) / ownerId(Long) / activeTaskCount(Long)。
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT project_id AS projectId,
|
||||
owner_id AS ownerId,
|
||||
CAST(COUNT(*) AS SIGNED) AS activeTaskCount
|
||||
FROM rdms_task
|
||||
WHERE deleted = b'0'
|
||||
AND owner_id IS NOT NULL
|
||||
AND project_id IN
|
||||
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
|
||||
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||
AND status_code NOT IN
|
||||
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||
</if>
|
||||
GROUP BY project_id, owner_id
|
||||
</script>
|
||||
""")
|
||||
List<Map<String, Object>> selectActiveTaskCountGroupByProjectIdAndOwner(
|
||||
@Param("projectIds") Collection<Long> projectIds,
|
||||
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes);
|
||||
|
||||
}
|
||||
|
||||
@@ -79,6 +79,18 @@ public interface ObjectStatusTransitionMapper extends BaseMapperX<ObjectStatusTr
|
||||
.eq(ObjectStatusTransitionDO::getToStatusCode, statusCode)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 反查动作中文名:同 objectType + action_code 下 action_name 唯一一致(已核实),取任一行。
|
||||
* 供错误提示等用户可见文案使用;查不到返回 null,由上层回退到原 actionCode。
|
||||
*/
|
||||
default String selectActionNameByObjectTypeAndAction(String objectType, String actionCode) {
|
||||
List<ObjectStatusTransitionDO> list = selectList(new LambdaQueryWrapperX<ObjectStatusTransitionDO>()
|
||||
.eq(ObjectStatusTransitionDO::getObjectType, objectType)
|
||||
.eq(ObjectStatusTransitionDO::getActionCode, actionCode)
|
||||
.last("LIMIT 1"));
|
||||
return list.isEmpty() ? null : list.get(0).getActionName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理删除
|
||||
*/
|
||||
|
||||
@@ -32,4 +32,10 @@ public @interface CheckObjectPermission {
|
||||
*/
|
||||
boolean memberOnly() default false;
|
||||
|
||||
/**
|
||||
* 是否走「可访问性门禁」:显式成员 OR 数据范围 scope 兜底(与 getXxxContext 入口口径一致)。
|
||||
* 为 true 时切面调用 checkAccessible,忽略 permission / memberOnly(优先级 accessible > memberOnly > permission)。
|
||||
*/
|
||||
boolean accessible() default false;
|
||||
|
||||
}
|
||||
|
||||
@@ -41,8 +41,13 @@ public class ObjectPermissionAspect {
|
||||
throw invalidParamException("暂不支持对象类型:{}", checkObjectPermission.objectType());
|
||||
}
|
||||
Long objectId = resolveObjectId(joinPoint, checkObjectPermission.objectId());
|
||||
permissionService.checkPermission(objectId, checkObjectPermission.permission(),
|
||||
checkObjectPermission.memberOnly());
|
||||
// 分发优先级:accessible(可访问性门禁)> memberOnly / permission(权限码)
|
||||
if (checkObjectPermission.accessible()) {
|
||||
permissionService.checkAccessible(objectId);
|
||||
} else {
|
||||
permissionService.checkPermission(objectId, checkObjectPermission.permission(),
|
||||
checkObjectPermission.memberOnly());
|
||||
}
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,4 +31,12 @@ public interface ObjectPermissionService {
|
||||
*/
|
||||
boolean hasPermission(Long objectId, String permission);
|
||||
|
||||
/**
|
||||
* 可访问性门禁:当前登录用户是否「能进入」该对象(显式成员 OR 数据范围 scope 兜底)。
|
||||
* 不可访问(含对象不存在)一律抛 ..._OBJECT_PERMISSION_DENIED,不暴露对象是否存在(见 spec §3.3)。
|
||||
*
|
||||
* @param objectId 对象编号
|
||||
*/
|
||||
void checkAccessible(Long objectId);
|
||||
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -32,6 +36,10 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Resource
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Resource
|
||||
private ProductMapper productMapper;
|
||||
@Resource
|
||||
private ObjectDataScopeService objectDataScopeService;
|
||||
|
||||
@Override
|
||||
public String getObjectType() {
|
||||
@@ -57,8 +65,9 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
||||
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||
.selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
||||
if (userRoles.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED,
|
||||
buildDeniedPermission(permission, memberOnly));
|
||||
// 权限码/成员标记仅作技术诊断,落日志不外泄给用户(见 用户可见错误文案规范)
|
||||
log.warn("[checkPermission] 用户无对象角色,objectId={}, permission={}, memberOnly={}", objectId, permission, memberOnly);
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
if (memberOnly) {
|
||||
return;
|
||||
@@ -70,7 +79,34 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
||||
.distinct()
|
||||
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
||||
if (!allowed) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED, normalizedPermission);
|
||||
log.warn("[checkPermission] 缺少对象权限码,objectId={}, permission={}", objectId, normalizedPermission);
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkAccessible(Long objectId) {
|
||||
if (objectId == null) {
|
||||
throw invalidParamException("对象编号不能为空");
|
||||
}
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
// 显式成员:拥有任一 ACTIVE 对象角色即可访问
|
||||
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||
.selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
||||
if (!userRoles.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 无显式角色:查对象拿 directionCode,按数据范围 scope 兜底(隐式 observer / 超管 ALL)
|
||||
ProductDO product = productMapper.selectById(objectId);
|
||||
if (product == null) {
|
||||
// spec §3.3 定稿:对象不存在一律 DENIED,不暴露存在性(技术诊断落 log.warn)
|
||||
log.warn("[checkAccessible] 对象不存在或无访问权,objectId={}", objectId);
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
|
||||
if (!scope.contains(objectId, product.getDirectionCode())) {
|
||||
log.warn("[checkAccessible] 无对象访问权,objectId={}", objectId);
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +131,4 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
||||
return permission.trim();
|
||||
}
|
||||
|
||||
private String buildDeniedPermission(String permission, boolean memberOnly) {
|
||||
return memberOnly ? "member" : normalizePermission(permission);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,12 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -33,6 +37,10 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Resource
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Resource
|
||||
private ProjectMapper projectMapper;
|
||||
@Resource
|
||||
private ObjectDataScopeService objectDataScopeService;
|
||||
|
||||
@Override
|
||||
public String getObjectType() {
|
||||
@@ -49,8 +57,9 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
||||
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
||||
if (userRoles.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED,
|
||||
buildDeniedPermission(permission, memberOnly));
|
||||
// 权限码/成员标记仅作技术诊断,落日志不外泄给用户(见 用户可见错误文案规范)
|
||||
log.warn("[checkPermission] 用户无对象角色,objectId={}, permission={}, memberOnly={}", objectId, permission, memberOnly);
|
||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
if (memberOnly) {
|
||||
return;
|
||||
@@ -62,7 +71,34 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
||||
.distinct()
|
||||
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
||||
if (!allowed) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED, normalizedPermission);
|
||||
log.warn("[checkPermission] 缺少对象权限码,objectId={}, permission={}", objectId, normalizedPermission);
|
||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkAccessible(Long objectId) {
|
||||
if (objectId == null) {
|
||||
throw invalidParamException("对象编号不能为空");
|
||||
}
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
// 显式成员:拥有任一 ACTIVE 对象角色即可访问
|
||||
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
||||
if (!userRoles.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 无显式角色:查对象拿 directionCode,按数据范围 scope 兜底(隐式 observer / 超管 ALL)
|
||||
ProjectDO project = projectMapper.selectById(objectId);
|
||||
if (project == null) {
|
||||
// spec §3.3 定稿:对象不存在一律 DENIED,不暴露存在性(技术诊断落 log.warn)
|
||||
log.warn("[checkAccessible] 对象不存在或无访问权,objectId={}", objectId);
|
||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProjectObjectConstants.OBJECT_TYPE);
|
||||
if (!scope.contains(objectId, project.getDirectionCode())) {
|
||||
log.warn("[checkAccessible] 无对象访问权,objectId={}", objectId);
|
||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +149,4 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
||||
return permission.trim();
|
||||
}
|
||||
|
||||
private String buildDeniedPermission(String permission, boolean memberOnly) {
|
||||
return memberOnly ? "member" : normalizePermission(permission);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationStatus
|
||||
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.service.status.StatusActionTextResolver;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -58,6 +59,8 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic
|
||||
@Resource
|
||||
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Override
|
||||
@@ -266,10 +269,13 @@ public class OvertimeApplicationServiceImpl implements OvertimeApplicationServic
|
||||
.selectByObjectTypeAndFromStatusAndAction(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, fromStatus,
|
||||
actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, fromStatus),
|
||||
statusActionTextResolver.actionName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED, actionCode);
|
||||
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED,
|
||||
statusActionTextResolver.actionName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
ObjectStatusModelDO toModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||
OvertimeApplicationConstants.STATUS_OBJECT_TYPE, transition.getToStatusCode());
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMappe
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
|
||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
|
||||
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -83,6 +84,8 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -170,11 +173,13 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus),
|
||||
statusActionTextResolver.actionName(PersonalItemConstants.STATUS_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
String reason = normalizeNullableText(reqVO.getReason());
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||
}
|
||||
String toStatus = transition.getToStatusCode();
|
||||
int updateCount = personalItemMapper.updateStatusByIdAndStatus(item.getId(), fromStatus, toStatus, reason);
|
||||
|
||||
@@ -65,6 +65,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", accessible = true)
|
||||
public List<ProductMemberRespVO> getProductMemberList(Long productId) {
|
||||
ProductDO product = validateProductExists(productId);
|
||||
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProductObjectConstants.OBJECT_TYPE, productId);
|
||||
|
||||
@@ -125,6 +125,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Resource
|
||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||
@Resource
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
// ========== 需求增删改查 ==========
|
||||
|
||||
@@ -1229,7 +1231,9 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
*/
|
||||
private void validateReviewRejectedActionAllowed(ProductRequirementDO requirement, String actionCode) {
|
||||
if (!isReviewRejectedActionAllowed(requirement, actionCode)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode()),
|
||||
statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1533,7 +1537,9 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
ObjectStatusTransitionDO transition = statusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, fromStatusCode),
|
||||
statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
return transition;
|
||||
}
|
||||
@@ -1544,7 +1550,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
||||
@VisibleForTesting
|
||||
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@ public class ProductServiceImpl implements ProductService {
|
||||
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
||||
@Resource
|
||||
private ObjectDataScopeService objectDataScopeService;
|
||||
@Resource
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -274,7 +276,7 @@ public class ProductServiceImpl implements ProductService {
|
||||
// 显式角色为空:走 scope.contains 判定隐式 observer 兜底(设计文档 2.1 节末段)
|
||||
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
|
||||
if (!scope.contains(id, product.getDirectionCode())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED, "查看");
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
return buildImplicitObserverContext(product);
|
||||
}
|
||||
@@ -568,7 +570,9 @@ public class ProductServiceImpl implements ProductService {
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(ProductObjectConstants.OBJECT_TYPE, fromStatusCode, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(ProductObjectConstants.OBJECT_TYPE, fromStatusCode),
|
||||
statusActionTextResolver.actionName(ProductObjectConstants.OBJECT_TYPE, actionCode));
|
||||
}
|
||||
return transition;
|
||||
}
|
||||
@@ -576,7 +580,7 @@ public class ProductServiceImpl implements ProductService {
|
||||
@VisibleForTesting
|
||||
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductS
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingRespVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -34,6 +36,7 @@ public class ProductSettingServiceImpl implements ProductSettingService {
|
||||
private ProductActivityTimelineQueryService productActivityTimelineQueryService;
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", accessible = true)
|
||||
public ProductSettingRespVO getProductSettings(Long productId) {
|
||||
ProductDO product = validateProductExists(productId);
|
||||
ProductSettingRespVO respVO = new ProductSettingRespVO();
|
||||
@@ -43,12 +46,14 @@ public class ProductSettingServiceImpl implements ProductSettingService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", accessible = true)
|
||||
public PageResult<ProductActivityRespVO> getProductActivities(Long productId, ProductActivityPageReqVO reqVO) {
|
||||
validateProductExists(productId);
|
||||
return productActivityQueryService.getProductActivities(productId, reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", accessible = true)
|
||||
public PageResult<ProductActivityTimelineRespVO> getProductActivityTimelinePage(
|
||||
Long productId, ProductActivityTimelinePageReqVO reqVO) {
|
||||
validateProductExists(productId);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.service.project;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectOwnedRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectParticipatedRespVO;
|
||||
|
||||
/**
|
||||
* 工作台「我的项目」Service:按登录用户隐式聚合,无权限注解。
|
||||
*/
|
||||
public interface MyProjectService {
|
||||
|
||||
/** 我参与的项目(作为成员) */
|
||||
PageResult<MyProjectParticipatedRespVO> getMyParticipatedPage(MyProjectPageReqVO reqVO);
|
||||
|
||||
/** 我负责的项目(managerUserId = 登录用户) */
|
||||
PageResult<MyProjectOwnedRespVO> getMyOwnedPage(MyProjectPageReqVO reqVO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package com.njcn.rdms.module.project.service.project;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||
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.controller.admin.project.project.vo.myproject.MyProjectOwnedRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectParticipatedRespVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||
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.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class MyProjectServiceImpl implements MyProjectService {
|
||||
|
||||
@Resource
|
||||
private ProjectMapper projectMapper;
|
||||
@Resource
|
||||
private ProjectTaskMapper projectTaskMapper;
|
||||
@Resource
|
||||
private ProjectExecutionMapper projectExecutionMapper;
|
||||
@Resource
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Resource
|
||||
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||
@Resource
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
/** 工作台「我的项目」列表统一排序:按项目创建时间升序(先创建的在前),id 兜底保证稳定。 */
|
||||
private static final Comparator<ProjectDO> PROJECT_CREATE_TIME_ASC =
|
||||
Comparator.comparing(ProjectDO::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(ProjectDO::getId);
|
||||
|
||||
@Override
|
||||
public PageResult<MyProjectParticipatedRespVO> getMyParticipatedPage(MyProjectPageReqVO reqVO) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
// 1. 我参与的所有 active 角色行(含 manager/dev 等多角色,objectId=项目id)
|
||||
List<UserObjectRoleDO> myRoles = userObjectRoleMapper
|
||||
.selectActiveListByObjectTypeAndUserId(ProjectObjectConstants.OBJECT_TYPE, loginUserId);
|
||||
if (myRoles.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
// 2. 按项目分组我的角色行
|
||||
Map<Long, List<UserObjectRoleDO>> rolesByProject = myRoles.stream()
|
||||
.filter(r -> r.getObjectId() != null)
|
||||
.collect(Collectors.groupingBy(UserObjectRoleDO::getObjectId, LinkedHashMap::new, Collectors.toList()));
|
||||
Set<Long> projectIds = new LinkedHashSet<>(rolesByProject.keySet());
|
||||
// 3. 项目基本信息
|
||||
List<ProjectDO> projects = projectMapper.selectBatchIds(projectIds);
|
||||
if (projects.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
// 3.1 仅保留非终态项目(终态项目不在工作台「我的项目」体现),并按创建时间升序(先创建的在前)
|
||||
List<String> projectTerminal = objectStatusModelMapper
|
||||
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE);
|
||||
projects = projects.stream()
|
||||
.filter(p -> !projectTerminal.contains(p.getStatusCode()))
|
||||
.sorted(PROJECT_CREATE_TIME_ASC)
|
||||
.collect(Collectors.toList());
|
||||
if (projects.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
// 4. statusName 批量回填
|
||||
Map<String, String> statusNameMap = loadStatusNameMap(ProjectObjectConstants.OBJECT_TYPE);
|
||||
// 5. 角色名 map(一次性拉全部涉及 roleId)
|
||||
Map<Long, ObjectRoleRespDTO> roleMap = loadRoleMap(myRoles.stream()
|
||||
.map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet()));
|
||||
// 5.1 每个项目下"我的可见角色行":剔除 visible=0 的隐式角色(创建者 / 隐式观察者等业务自动赋予角色)。
|
||||
// 若某项目下我没有任何可见角色,则不算"我参与的项目",整项剔除——与 ProjectMemberServiceImpl 团队列表口径一致。
|
||||
Map<Long, List<UserObjectRoleDO>> visibleRolesByProject = new LinkedHashMap<>();
|
||||
rolesByProject.forEach((pid, rows) -> {
|
||||
List<UserObjectRoleDO> visible = filterVisibleRoleRows(rows, roleMap);
|
||||
if (!visible.isEmpty()) {
|
||||
visibleRolesByProject.put(pid, visible);
|
||||
}
|
||||
});
|
||||
// 6. 我负责的任务计数(owner=me,按项目分组 total + pending)
|
||||
List<String> taskTerminal = objectStatusModelMapper
|
||||
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||
Map<Long, long[]> taskCountMap = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : projectTaskMapper
|
||||
.selectMyTaskCountGroupByProjectIds(loginUserId, projectIds, taskTerminal)) {
|
||||
taskCountMap.put(asLong(row.get("projectId")),
|
||||
new long[]{asLong(row.get("totalCount")), asLong(row.get("pendingCount"))});
|
||||
}
|
||||
// 7. 组装(仅保留我有可见角色的项目)
|
||||
List<MyProjectParticipatedRespVO> all = projects.stream()
|
||||
.filter(p -> visibleRolesByProject.containsKey(p.getId()))
|
||||
.map(p -> {
|
||||
MyProjectParticipatedRespVO vo = new MyProjectParticipatedRespVO();
|
||||
vo.setId(p.getId());
|
||||
vo.setName(p.getProjectName());
|
||||
vo.setCode(p.getProjectCode());
|
||||
vo.setStatusCode(p.getStatusCode());
|
||||
vo.setStatusName(statusNameMap.get(p.getStatusCode()));
|
||||
vo.setProgress(toProgressInt(p.getProgressRate()));
|
||||
vo.setMyRole(buildMyRole(visibleRolesByProject.get(p.getId()), roleMap));
|
||||
long[] c = taskCountMap.getOrDefault(p.getId(), new long[]{0L, 0L});
|
||||
vo.setMyTaskCount((int) c[0]);
|
||||
vo.setMyPendingTaskCount((int) c[1]);
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
return paginate(all, reqVO);
|
||||
}
|
||||
|
||||
// ======================== 共用私有方法 ========================
|
||||
|
||||
private Map<String, String> loadStatusNameMap(String objectType) {
|
||||
return objectStatusModelMapper.selectListByObjectTypeEnabled(objectType).stream()
|
||||
.collect(Collectors.toMap(ObjectStatusModelDO::getStatusCode,
|
||||
ObjectStatusModelDO::getStatusName, (a, b) -> a));
|
||||
}
|
||||
|
||||
private Map<Long, ObjectRoleRespDTO> loadRoleMap(Set<Long> roleIds) {
|
||||
if (roleIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ObjectRoleRespDTO> roles = objectPermissionApi
|
||||
.getObjectRoleList(roleIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
||||
.getCheckedData();
|
||||
if (roles == null || roles.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return roles.stream().collect(Collectors.toMap(ObjectRoleRespDTO::getId, Function.identity(), (a, b) -> a));
|
||||
}
|
||||
|
||||
/** 可见角色行:剔除 visible=0 的隐式角色(创建者 / 隐式观察者等业务自动赋予角色);visible=null 或 roleMap 缺失视同可见。 */
|
||||
private List<UserObjectRoleDO> filterVisibleRoleRows(List<UserObjectRoleDO> rows, Map<Long, ObjectRoleRespDTO> roleMap) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return rows.stream()
|
||||
.filter(r -> {
|
||||
ObjectRoleRespDTO role = roleMap.get(r.getRoleId());
|
||||
return role == null || !Integer.valueOf(0).equals(role.getVisible());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 主角色 + 附加角色名拼接。入参为已过滤的可见角色行(visible=0 隐式角色已在上游剔除)。
|
||||
* 主角色挑选与 ProjectMemberServiceImpl 一致:MANAGER 优先,否则 roleId 最小。
|
||||
*/
|
||||
private String buildMyRole(List<UserObjectRoleDO> rowsVisible, Map<Long, ObjectRoleRespDTO> roleMap) {
|
||||
if (rowsVisible == null || rowsVisible.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
UserObjectRoleDO primary = rowsVisible.stream()
|
||||
.filter(r -> {
|
||||
ObjectRoleRespDTO role = roleMap.get(r.getRoleId());
|
||||
return role != null && Objects.equals(ProjectObjectConstants.MANAGER_ROLE_CODE, role.getCode());
|
||||
})
|
||||
.findFirst()
|
||||
.orElseGet(() -> rowsVisible.stream()
|
||||
.min(Comparator.comparing(UserObjectRoleDO::getRoleId))
|
||||
.orElse(rowsVisible.get(0)));
|
||||
String primaryName = roleName(roleMap, primary.getRoleId());
|
||||
List<String> additional = rowsVisible.stream()
|
||||
.filter(r -> !Objects.equals(r.getId(), primary.getId()))
|
||||
.map(r -> roleName(roleMap, r.getRoleId()))
|
||||
.filter(Objects::nonNull)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
StringBuilder sb = new StringBuilder(primaryName == null ? "" : primaryName);
|
||||
for (String n : additional) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(" / ");
|
||||
}
|
||||
sb.append(n);
|
||||
}
|
||||
return sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
|
||||
private String roleName(Map<Long, ObjectRoleRespDTO> roleMap, Long roleId) {
|
||||
ObjectRoleRespDTO role = roleMap.get(roleId);
|
||||
return role == null ? null : role.getName();
|
||||
}
|
||||
|
||||
private Integer toProgressInt(BigDecimal v) {
|
||||
return v == null ? 0 : v.setScale(0, RoundingMode.HALF_UP).intValue();
|
||||
}
|
||||
|
||||
private long asLong(Object v) {
|
||||
return v == null ? 0L : ((Number) v).longValue();
|
||||
}
|
||||
|
||||
private <T> PageResult<T> paginate(List<T> all, PageParam reqVO) {
|
||||
long total = all.size();
|
||||
Integer pageSize = reqVO.getPageSize();
|
||||
if (pageSize == null || pageSize < 0) {
|
||||
return new PageResult<>(all, total);
|
||||
}
|
||||
int pageNo = reqVO.getPageNo() == null || reqVO.getPageNo() < 1 ? 1 : reqVO.getPageNo();
|
||||
int fromIndex = Math.min((pageNo - 1) * pageSize, all.size());
|
||||
int toIndex = Math.min(fromIndex + pageSize, all.size());
|
||||
return new PageResult<>(all.subList(fromIndex, toIndex), total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<MyProjectOwnedRespVO> getMyOwnedPage(MyProjectPageReqVO reqVO) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
// 1. 我负责的项目(managerUserId = 登录用户)
|
||||
List<ProjectDO> projects = projectMapper.selectList(new LambdaQueryWrapperX<ProjectDO>()
|
||||
.eq(ProjectDO::getManagerUserId, loginUserId));
|
||||
if (projects.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
// 1.1 仅保留非终态项目(终态项目不在工作台「我的项目」体现),并按创建时间升序(先创建的在前)
|
||||
List<String> projectTerminal = objectStatusModelMapper
|
||||
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE);
|
||||
projects = projects.stream()
|
||||
.filter(p -> !projectTerminal.contains(p.getStatusCode()))
|
||||
.sorted(PROJECT_CREATE_TIME_ASC)
|
||||
.collect(Collectors.toList());
|
||||
if (projects.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
Set<Long> projectIds = projects.stream()
|
||||
.map(ProjectDO::getId).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
// 2. 终态集
|
||||
List<String> taskTerminal = objectStatusModelMapper
|
||||
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||
List<String> execTerminal = objectStatusModelMapper
|
||||
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
||||
LocalDate today = LocalDate.now();
|
||||
// 3. 任务数 + 逾期数(一次扫表)
|
||||
Map<Long, long[]> taskMap = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : projectTaskMapper
|
||||
.selectTaskAndOverdueCountGroupByProjectIds(projectIds, taskTerminal, today)) {
|
||||
taskMap.put(asLong(row.get("projectId")),
|
||||
new long[]{asLong(row.get("taskCount")), asLong(row.get("overdueCount"))});
|
||||
}
|
||||
// 4. 执行数
|
||||
Map<Long, Long> execMap = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : projectExecutionMapper
|
||||
.selectExecutionCountGroupByProjectIds(projectIds, execTerminal)) {
|
||||
execMap.put(asLong(row.get("projectId")), asLong(row.get("executionCount")));
|
||||
}
|
||||
// 5. 每个负责人(owner)的进行中任务数:projectId -> (ownerId -> count)
|
||||
Map<Long, Map<Long, Long>> activeTaskMap = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : projectTaskMapper
|
||||
.selectActiveTaskCountGroupByProjectIdAndOwner(projectIds, taskTerminal)) {
|
||||
Long pid = asLong(row.get("projectId"));
|
||||
Long ownerId = asLong(row.get("ownerId"));
|
||||
activeTaskMap.computeIfAbsent(pid, k -> new LinkedHashMap<>())
|
||||
.put(ownerId, asLong(row.get("activeTaskCount")));
|
||||
}
|
||||
// 6. 成员清单(批量一次拿全,内存按项目分组;同 user 多角色去重为一个成员)
|
||||
List<UserObjectRoleDO> memberRows = userObjectRoleMapper
|
||||
.selectActiveListByObjectTypeAndObjectIds(ProjectObjectConstants.OBJECT_TYPE, projectIds);
|
||||
Map<Long, List<Long>> memberUserIdsByProject = new LinkedHashMap<>();
|
||||
for (UserObjectRoleDO m : memberRows) {
|
||||
if (m.getObjectId() == null || m.getUserId() == null) {
|
||||
continue;
|
||||
}
|
||||
List<Long> users = memberUserIdsByProject.computeIfAbsent(m.getObjectId(), k -> new ArrayList<>());
|
||||
if (!users.contains(m.getUserId())) {
|
||||
users.add(m.getUserId());
|
||||
}
|
||||
}
|
||||
// 7. 成员昵称批量回填
|
||||
Set<Long> allUserIds = memberUserIdsByProject.values().stream()
|
||||
.flatMap(List::stream).collect(Collectors.toSet());
|
||||
Map<Long, AdminUserRespDTO> userMap = allUserIds.isEmpty()
|
||||
? Collections.emptyMap() : adminUserApi.getUserMap(allUserIds);
|
||||
// 8. myRole 恒为负责人角色名(一次性解析)
|
||||
String managerRoleName = resolveManagerRoleName();
|
||||
// 9. 组装
|
||||
List<MyProjectOwnedRespVO> all = projects.stream().map(p -> {
|
||||
MyProjectOwnedRespVO vo = new MyProjectOwnedRespVO();
|
||||
vo.setId(p.getId());
|
||||
vo.setName(p.getProjectName());
|
||||
vo.setCode(p.getProjectCode());
|
||||
vo.setProgress(toProgressInt(p.getProgressRate()));
|
||||
vo.setMyRole(managerRoleName);
|
||||
vo.setPlannedEndDate(p.getPlannedEndDate());
|
||||
long[] tc = taskMap.getOrDefault(p.getId(), new long[]{0L, 0L});
|
||||
vo.setTaskCount((int) tc[0]);
|
||||
vo.setOverdueCount((int) tc[1]);
|
||||
vo.setExecutionCount(execMap.getOrDefault(p.getId(), 0L).intValue());
|
||||
List<Long> memberUserIds = memberUserIdsByProject.getOrDefault(p.getId(), Collections.emptyList());
|
||||
vo.setMemberCount(memberUserIds.size());
|
||||
Map<Long, Long> ownerCounts = activeTaskMap.getOrDefault(p.getId(), Collections.emptyMap());
|
||||
List<MyProjectOwnedRespVO.MemberLoadVO> members = memberUserIds.stream().map(uid -> {
|
||||
MyProjectOwnedRespVO.MemberLoadVO mv = new MyProjectOwnedRespVO.MemberLoadVO();
|
||||
mv.setUserId(uid);
|
||||
AdminUserRespDTO user = userMap.get(uid);
|
||||
mv.setUserName(user == null ? null : user.getNickname());
|
||||
mv.setActiveTaskCount(ownerCounts.getOrDefault(uid, 0L).intValue());
|
||||
return mv;
|
||||
}).collect(Collectors.toList());
|
||||
vo.setMembers(members);
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
return paginate(all, reqVO);
|
||||
}
|
||||
|
||||
/** 项目负责人角色名(对象域 MANAGER_ROLE_CODE);解析失败返回 null,不阻断列表。 */
|
||||
private String resolveManagerRoleName() {
|
||||
try {
|
||||
ObjectRoleRespDTO role = objectPermissionApi
|
||||
.getObjectRoleByCode(ProjectObjectConstants.MANAGER_ROLE_CODE,
|
||||
ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
||||
.getCheckedData();
|
||||
return role == null ? null : role.getName();
|
||||
} catch (RuntimeException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,6 +69,7 @@ public class ProjectMemberServiceImpl implements ProjectMemberService {
|
||||
private ProjectExecutionMapper projectExecutionMapper;
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", accessible = true)
|
||||
public List<ProjectMemberRespVO> getProjectMemberList(Long projectId) {
|
||||
ProjectDO project = validateProjectExists(projectId);
|
||||
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId);
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMappe
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
|
||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
|
||||
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -131,6 +132,8 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||
@Resource
|
||||
private ProjectExecutionMapper projectExecutionMapper;
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -882,7 +885,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
*/
|
||||
private void validateReviewRejectedActionAllowed(ProjectRequirementDO requirement, String actionCode) {
|
||||
if (!isReviewRejectedActionAllowed(requirement, actionCode)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode()),
|
||||
statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1297,7 +1302,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
ObjectStatusTransitionDO transition = statusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, fromStatusCode),
|
||||
statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
return transition;
|
||||
}
|
||||
@@ -1305,7 +1312,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
||||
@VisibleForTesting
|
||||
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
||||
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectMenuRespDTO;
|
||||
@@ -116,6 +117,8 @@ class ProjectServiceImpl implements ProjectService {
|
||||
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
||||
@Resource
|
||||
private ObjectDataScopeService objectDataScopeService;
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -393,7 +396,7 @@ class ProjectServiceImpl implements ProjectService {
|
||||
// 显式角色为空:走 scope.contains 判定隐式 observer 兜底(设计文档 2.1 节末段)
|
||||
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProjectObjectConstants.OBJECT_TYPE);
|
||||
if (!scope.contains(id, project.getDirectionCode())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED, "查看");
|
||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||
}
|
||||
return buildImplicitObserverContext(project);
|
||||
}
|
||||
@@ -580,7 +583,9 @@ class ProjectServiceImpl implements ProjectService {
|
||||
ProjectDO project = validateProjectExists(reqVO.getId());
|
||||
String actionCode = reqVO.getActionCode().trim();
|
||||
if (ObjectActivityConstants.PROJECT_ACTION_AUTO_START.equals(actionCode)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode()),
|
||||
statusActionTextResolver.actionName(ProjectObjectConstants.OBJECT_TYPE, actionCode));
|
||||
}
|
||||
changeStatus(project, actionCode, normalizeNullableText(reqVO.getReason()));
|
||||
}
|
||||
@@ -613,10 +618,12 @@ class ProjectServiceImpl implements ProjectService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void autoStartProjectIfPending(Long projectId, String triggerAction) {
|
||||
// auto_start 只允许由后端业务动作内部触发,前端不应直接透传该动作。
|
||||
if (!ProjectObjectConstants.AUTO_START_TRIGGERS.contains(triggerAction)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED, triggerAction);
|
||||
}
|
||||
ProjectDO project = validateProjectExists(projectId);
|
||||
if (!ProjectObjectConstants.AUTO_START_TRIGGERS.contains(triggerAction)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode()),
|
||||
statusActionTextResolver.actionName(ProjectObjectConstants.OBJECT_TYPE, triggerAction));
|
||||
}
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode(),
|
||||
ObjectActivityConstants.PROJECT_ACTION_AUTO_START);
|
||||
@@ -625,7 +632,9 @@ class ProjectServiceImpl implements ProjectService {
|
||||
ObjectStatusModelDO statusModel = validateEnabledStatusModel(project.getStatusCode());
|
||||
if (Boolean.TRUE.equals(statusModel.getInitialFlag())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
||||
ObjectActivityConstants.PROJECT_ACTION_AUTO_START);
|
||||
statusActionTextResolver.statusName(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode()),
|
||||
statusActionTextResolver.actionName(ProjectObjectConstants.OBJECT_TYPE,
|
||||
ObjectActivityConstants.PROJECT_ACTION_AUTO_START));
|
||||
}
|
||||
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_NOT_ALLOW_EDIT);
|
||||
@@ -772,10 +781,12 @@ class ProjectServiceImpl implements ProjectService {
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(ProjectObjectConstants.OBJECT_TYPE, fromStatusCode, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(ProjectObjectConstants.OBJECT_TYPE, fromStatusCode),
|
||||
statusActionTextResolver.actionName(ProjectObjectConstants.OBJECT_TYPE, actionCode));
|
||||
}
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||
}
|
||||
return transition;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ public class ProjectExecutionAssigneeServiceImpl implements ProjectExecutionAssi
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||
public List<ExecutionAssigneeRespVO> getExecutionAssigneeList(Long projectId, Long executionId) {
|
||||
validateProjectExists(projectId);
|
||||
validateExecutionExists(projectId, executionId);
|
||||
@@ -150,6 +152,8 @@ public class ProjectExecutionAssigneeServiceImpl implements ProjectExecutionAssi
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||
public PageResult<ExecutionAssigneeLogRespVO> getExecutionAssigneeLogPage(Long projectId, Long executionId,
|
||||
ExecutionAssigneeLogPageReqVO reqVO) {
|
||||
validateProjectExists(projectId);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.njcn.rdms.module.project.service.project.execution;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
|
||||
@@ -38,6 +40,13 @@ public interface ProjectExecutionService {
|
||||
|
||||
List<ProjectExecutionRespVO> getCurrentUserExecutionList();
|
||||
|
||||
/**
|
||||
* 分页查询当前登录用户作为负责人(owner)的执行,跨所有项目聚合。
|
||||
* 默认口径:排除终态状态(completed/cancelled)且排除进度已满(progressRate >= 100)的执行。
|
||||
* pageSize 传 -1(PageParam.PAGE_SIZE_NONE)= 返回全部、不切片。
|
||||
*/
|
||||
PageResult<MyProjectExecutionRespVO> getMyExecutionPage(MyProjectExecutionPageReqVO reqVO);
|
||||
|
||||
void changeOwner(Long projectId, Long executionId, ProjectExecutionOwnerChangeReqVO reqVO);
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
|
||||
@@ -46,6 +48,7 @@ import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPer
|
||||
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.task.ProjectTaskService;
|
||||
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
@@ -118,6 +121,8 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
private ProjectRequirementService projectRequirementService;
|
||||
@Resource
|
||||
private ProjectRequirementMapper projectRequirementMapper;
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
/**
|
||||
* 任务服务:执行 cancel / pause / resume 时级联调任务侧 internal 入口。
|
||||
* 与 ProjectTaskService 互相依赖(任务侧已注入 ProjectExecutionService 用于通知执行),用 @Lazy 打破循环。
|
||||
@@ -302,6 +307,91 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<MyProjectExecutionRespVO> getMyExecutionPage(MyProjectExecutionPageReqVO reqVO) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
List<ProjectExecutionDO> executions = projectExecutionMapper.selectListByOwnerId(loginUserId);
|
||||
if (executions == null || executions.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
// 1. 排除终态状态(completed/cancelled,DB 权威,不硬编码)
|
||||
List<String> terminalStatusCodes =
|
||||
objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
||||
List<ProjectExecutionDO> nonTerminal = executions.stream()
|
||||
.filter(e -> terminalStatusCodes == null || !terminalStatusCodes.contains(e.getStatusCode()))
|
||||
.collect(Collectors.toList());
|
||||
if (nonTerminal.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
// 2. 按 projectId 分组,逐组聚合一级任务真实进度(复用 loadExecutionProgressMap)
|
||||
List<String> excludedTaskStatusCodes = loadProgressExcludedTaskStatusCodes();
|
||||
Map<Long, BigDecimal> progressMap = new HashMap<>();
|
||||
nonTerminal.stream()
|
||||
.filter(e -> e.getProjectId() != null)
|
||||
.collect(Collectors.groupingBy(ProjectExecutionDO::getProjectId, LinkedHashMap::new, Collectors.toList()))
|
||||
.forEach((groupProjectId, groupList) -> {
|
||||
Set<Long> executionIds = groupList.stream()
|
||||
.map(ProjectExecutionDO::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
progressMap.putAll(loadExecutionProgressMap(groupProjectId, executionIds, excludedTaskStatusCodes));
|
||||
});
|
||||
// 3. 排除进度已满(progressRate >= 100);缺失进度按 0 处理
|
||||
BigDecimal full = BigDecimal.valueOf(100);
|
||||
List<ProjectExecutionDO> filtered = nonTerminal.stream()
|
||||
.filter(e -> progressMap.getOrDefault(e.getId(), normalizeProgress(null)).compareTo(full) < 0)
|
||||
.collect(Collectors.toList());
|
||||
if (filtered.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
// 4. 批量回填 projectName / statusName / projectRequirementName
|
||||
Set<Long> projectIds = filtered.stream()
|
||||
.map(ProjectExecutionDO::getProjectId).filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<Long, String> projectNameMap = projectIds.isEmpty() ? Collections.emptyMap()
|
||||
: projectMapper.selectBatchIds(projectIds).stream()
|
||||
.collect(Collectors.toMap(ProjectDO::getId, ProjectDO::getProjectName, (a, b) -> a));
|
||||
Map<String, String> statusNameMap = objectStatusModelMapper
|
||||
.selectListByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE).stream()
|
||||
.collect(Collectors.toMap(ObjectStatusModelDO::getStatusCode, ObjectStatusModelDO::getStatusName, (a, b) -> a));
|
||||
Set<Long> requirementIds = filtered.stream()
|
||||
.map(ProjectExecutionDO::getProjectRequirementId).filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<Long, String> requirementNameMap = requirementIds.isEmpty() ? Collections.emptyMap()
|
||||
: projectRequirementMapper.selectBatchIds(requirementIds).stream()
|
||||
.collect(Collectors.toMap(ProjectRequirementDO::getId, ProjectRequirementDO::getTitle, (a, b) -> a));
|
||||
// 5. 组装精简 VO(progressRate BigDecimal → Integer 四舍五入)
|
||||
List<MyProjectExecutionRespVO> all = filtered.stream().map(e -> {
|
||||
MyProjectExecutionRespVO vo = new MyProjectExecutionRespVO();
|
||||
vo.setId(e.getId());
|
||||
vo.setExecutionName(e.getExecutionName());
|
||||
vo.setProjectId(e.getProjectId());
|
||||
vo.setProjectName(projectNameMap.get(e.getProjectId()));
|
||||
vo.setStatusCode(e.getStatusCode());
|
||||
vo.setStatusName(statusNameMap.get(e.getStatusCode()));
|
||||
vo.setPriority(e.getPriority());
|
||||
vo.setPlannedStartDate(e.getPlannedStartDate());
|
||||
vo.setPlannedEndDate(e.getPlannedEndDate());
|
||||
vo.setActualStartDate(e.getActualStartDate());
|
||||
vo.setActualEndDate(e.getActualEndDate());
|
||||
BigDecimal progress = progressMap.getOrDefault(e.getId(), normalizeProgress(null));
|
||||
vo.setProgressRate(progress.setScale(0, RoundingMode.HALF_UP).intValue());
|
||||
vo.setProjectRequirementId(e.getProjectRequirementId());
|
||||
vo.setProjectRequirementName(requirementNameMap.get(e.getProjectRequirementId()));
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
// 6. 分页:pageSize<0(PAGE_SIZE_NONE)= 全部;否则内存切片(兼容,前端本期不使用)
|
||||
long total = all.size();
|
||||
Integer pageSize = reqVO.getPageSize();
|
||||
if (pageSize == null || pageSize < 0) {
|
||||
return new PageResult<>(all, total);
|
||||
}
|
||||
int pageNo = reqVO.getPageNo() == null || reqVO.getPageNo() < 1 ? 1 : reqVO.getPageNo();
|
||||
int fromIndex = Math.min((pageNo - 1) * pageSize, all.size());
|
||||
int toIndex = Math.min(fromIndex + pageSize, all.size());
|
||||
return new PageResult<>(all.subList(fromIndex, toIndex), total);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
@@ -489,10 +579,12 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(ProjectExecutionConstants.OBJECT_TYPE, fromStatusCode, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(ProjectExecutionConstants.OBJECT_TYPE, fromStatusCode),
|
||||
statusActionTextResolver.actionName(ProjectExecutionConstants.OBJECT_TYPE, actionCode));
|
||||
}
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||
}
|
||||
return transition;
|
||||
}
|
||||
@@ -1017,7 +1109,7 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (!Objects.equals(loginUserId, execution.getOwnerId())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_OWNER_ONLY,
|
||||
resolveActionDisplayName(actionCode));
|
||||
statusActionTextResolver.actionName(ProjectExecutionConstants.OBJECT_TYPE, actionCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,16 +1120,6 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
|| "resume".equals(actionCode);
|
||||
}
|
||||
|
||||
private String resolveActionDisplayName(String actionCode) {
|
||||
return switch (actionCode) {
|
||||
case "complete" -> "完成";
|
||||
case "cancel" -> "取消";
|
||||
case "pause" -> "暂停";
|
||||
case "resume" -> "恢复";
|
||||
default -> actionCode;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成执行前置校验:执行下所有任务必须已经进入终态(completed / cancelled)。
|
||||
*/
|
||||
|
||||
@@ -47,6 +47,7 @@ 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.task.assignee.TaskAssigneeService;
|
||||
import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogService;
|
||||
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||
@@ -127,6 +128,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
private ProjectObjectAuthorizationService projectObjectAuthorizationService;
|
||||
@Resource
|
||||
private DictDataApi dictDataApi;
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -673,7 +676,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (!Objects.equals(loginUserId, task.getOwnerId())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_STATUS_OWNER_ONLY,
|
||||
resolveActionDisplayName(actionCode));
|
||||
statusActionTextResolver.actionName(ProjectTaskConstants.OBJECT_TYPE, actionCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,16 +687,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
|| "resume".equals(actionCode);
|
||||
}
|
||||
|
||||
private String resolveActionDisplayName(String actionCode) {
|
||||
return switch (actionCode) {
|
||||
case "complete" -> "完成";
|
||||
case "cancel" -> "取消";
|
||||
case "pause" -> "暂停";
|
||||
case "resume" -> "恢复";
|
||||
default -> actionCode;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据通用状态语义位推导实际开始/结束日期:
|
||||
* - 首次离开初始态(fromStatus.initialFlag=true)且未填写时,写入 actualStartDate
|
||||
@@ -828,10 +821,12 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(ProjectTaskConstants.OBJECT_TYPE, fromStatusCode, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(ProjectTaskConstants.OBJECT_TYPE, fromStatusCode),
|
||||
statusActionTextResolver.actionName(ProjectTaskConstants.OBJECT_TYPE, actionCode));
|
||||
}
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||
}
|
||||
return transition;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ public class TaskAssigneeServiceImpl implements TaskAssigneeService {
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||
public List<TaskAssigneeRespVO> getAssigneeList(Long projectId, Long executionId, Long taskId) {
|
||||
validateExecutionAndTaskExists(projectId, executionId, taskId);
|
||||
List<TaskAssigneeDO> activeList = taskAssigneeMapper.selectActiveListByTaskId(taskId);
|
||||
@@ -121,6 +123,8 @@ public class TaskAssigneeServiceImpl implements TaskAssigneeService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||
public PageResult<TaskAssigneeLogRespVO> getAssigneeLogPage(Long projectId, Long executionId, Long taskId,
|
||||
TaskAssigneeLogPageReqVO reqVO) {
|
||||
validateExecutionAndTaskExists(projectId, executionId, taskId);
|
||||
|
||||
@@ -79,6 +79,8 @@ public class TaskWorklogServiceImpl implements TaskWorklogService {
|
||||
private ProjectTaskService projectTaskService;
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||
public PageResult<TaskWorklogRespVO> getWorklogPage(Long projectId, Long executionId, Long taskId,
|
||||
TaskWorklogPageReqVO reqVO) {
|
||||
validateExecutionAndTaskExists(projectId, executionId, taskId);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.njcn.rdms.module.project.service.status;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 状态机文案解析器:把动作 code / 状态 code 翻成 DB 状态机里的中文展示名,
|
||||
* 供错误提示等用户可见文案使用。查不到时回退原 code(不抛错)。
|
||||
* 权威源:rdms_object_status_transition.action_name / rdms_object_status_model.status_name。
|
||||
* 背景:TD-012(给用户看的 message 不外泄技术 token;技术诊断由 infra_api_access_log 承载)。
|
||||
*/
|
||||
@Component
|
||||
public class StatusActionTextResolver {
|
||||
|
||||
@Resource
|
||||
private ObjectStatusTransitionMapper transitionMapper;
|
||||
@Resource
|
||||
private ObjectStatusModelMapper statusModelMapper;
|
||||
|
||||
/** 动作中文名;空入参或查不到时回退原 actionCode。 */
|
||||
public String actionName(String objectType, String actionCode) {
|
||||
if (!StringUtils.hasText(actionCode)) {
|
||||
return actionCode;
|
||||
}
|
||||
String name = transitionMapper.selectActionNameByObjectTypeAndAction(objectType, actionCode);
|
||||
return StringUtils.hasText(name) ? name : actionCode;
|
||||
}
|
||||
|
||||
/** 状态中文名;空入参或查不到时回退原 statusCode。 */
|
||||
public String statusName(String objectType, String statusCode) {
|
||||
if (!StringUtils.hasText(statusCode)) {
|
||||
return statusCode;
|
||||
}
|
||||
ObjectStatusModelDO model = statusModelMapper.selectByObjectTypeAndStatusCode(objectType, statusCode);
|
||||
return model != null && StringUtils.hasText(model.getStatusName()) ? model.getStatusName() : statusCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.njcn.rdms.module.project.framework.security.aop;
|
||||
|
||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
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.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeLogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.framework.security.service.ObjectPermissionService;
|
||||
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionAssigneeService;
|
||||
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionAssigneeServiceImpl;
|
||||
import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeService;
|
||||
import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeServiceImpl;
|
||||
import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogService;
|
||||
import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 对象域内「协办人 / 工时」读路径的对象级鉴权回归测试(TD-001)。
|
||||
* <p>
|
||||
* 验证执行协办人、任务协办人、任务工时这 5 个读接口都已挂
|
||||
* {@link com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission},且:
|
||||
* <ul>
|
||||
* <li>{@code objectId="#projectId"} 解析为 projectId(而非 executionId/taskId);</li>
|
||||
* <li>permission 为同层 QUERY 码(执行域 project:execution:query、任务域 project:task:query);</li>
|
||||
* <li>无权(checkPermission 抛异常)时方法体被拦、不执行。</li>
|
||||
* </ul>
|
||||
* 沿用 {@link ObjectPermissionAspectTest} 的 {@link AspectJProxyFactory} 手动织入方式,纯单测、不连库、不起容器。
|
||||
* 真实的「有无权限」判定逻辑由 ProjectObjectPermissionServiceTest 覆盖,本类不重复。
|
||||
*/
|
||||
class AssigneeWorklogReadPermissionTest extends BaseMockitoUnitTest {
|
||||
|
||||
private static final Long PROJECT_ID = 1001L;
|
||||
private static final Long EXECUTION_ID = 2001L;
|
||||
private static final Long TASK_ID = 3001L;
|
||||
|
||||
@Mock
|
||||
private ObjectPermissionService objectPermissionService;
|
||||
|
||||
@Test
|
||||
void getExecutionAssigneeList_shouldGuardByExecutionQueryOnProjectId() {
|
||||
ProjectExecutionAssigneeService proxy =
|
||||
(ProjectExecutionAssigneeService) proxyOf(new ProjectExecutionAssigneeServiceImpl());
|
||||
denyPermission(ProjectExecutionConstants.PERMISSION_QUERY);
|
||||
|
||||
assertThrows(ServiceException.class, () -> proxy.getExecutionAssigneeList(PROJECT_ID, EXECUTION_ID));
|
||||
|
||||
verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectExecutionConstants.PERMISSION_QUERY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getExecutionAssigneeLogPage_shouldGuardByExecutionQueryOnProjectId() {
|
||||
ProjectExecutionAssigneeService proxy =
|
||||
(ProjectExecutionAssigneeService) proxyOf(new ProjectExecutionAssigneeServiceImpl());
|
||||
denyPermission(ProjectExecutionConstants.PERMISSION_QUERY);
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> proxy.getExecutionAssigneeLogPage(PROJECT_ID, EXECUTION_ID, new ExecutionAssigneeLogPageReqVO()));
|
||||
|
||||
verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectExecutionConstants.PERMISSION_QUERY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAssigneeList_shouldGuardByTaskQueryOnProjectId() {
|
||||
TaskAssigneeService proxy = (TaskAssigneeService) proxyOf(new TaskAssigneeServiceImpl());
|
||||
denyPermission(ProjectTaskConstants.PERMISSION_QUERY);
|
||||
|
||||
assertThrows(ServiceException.class, () -> proxy.getAssigneeList(PROJECT_ID, EXECUTION_ID, TASK_ID));
|
||||
|
||||
verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAssigneeLogPage_shouldGuardByTaskQueryOnProjectId() {
|
||||
TaskAssigneeService proxy = (TaskAssigneeService) proxyOf(new TaskAssigneeServiceImpl());
|
||||
denyPermission(ProjectTaskConstants.PERMISSION_QUERY);
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> proxy.getAssigneeLogPage(PROJECT_ID, EXECUTION_ID, TASK_ID, new TaskAssigneeLogPageReqVO()));
|
||||
|
||||
verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWorklogPage_shouldGuardByTaskQueryOnProjectId() {
|
||||
TaskWorklogService proxy = (TaskWorklogService) proxyOf(new TaskWorklogServiceImpl());
|
||||
denyPermission(ProjectTaskConstants.PERMISSION_QUERY);
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> proxy.getWorklogPage(PROJECT_ID, EXECUTION_ID, TASK_ID, new TaskWorklogPageReqVO()));
|
||||
|
||||
verify(objectPermissionService).checkPermission(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 AspectJProxyFactory 把对象权限切面织到真实 Service 上(JDK 代理,返回接口)。
|
||||
* target 为无依赖 new 出的实例:无权时切面在 proceed() 之前拦截,方法体不执行,故依赖为 null 无碍。
|
||||
*/
|
||||
private Object proxyOf(Object target) {
|
||||
when(objectPermissionService.getObjectType()).thenReturn(ProjectObjectConstants.OBJECT_TYPE);
|
||||
AspectJProxyFactory proxyFactory = new AspectJProxyFactory(target);
|
||||
proxyFactory.addAspect(new ObjectPermissionAspect(List.of(objectPermissionService)));
|
||||
return proxyFactory.getProxy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟「无权」:让对象权限校验抛出权限不足异常(与 ProjectObjectPermissionService 无权时一致)。
|
||||
*/
|
||||
private void denyPermission(String permission) {
|
||||
doThrow(exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED))
|
||||
.when(objectPermissionService).checkPermission(eq(PROJECT_ID), eq(permission), eq(false));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import org.mockito.Mock;
|
||||
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -43,6 +47,19 @@ class ObjectPermissionAspectTest extends BaseMockitoUnitTest {
|
||||
.checkPermission(1002L, "", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void around_whenAccessible_shouldDispatchToCheckAccessible() {
|
||||
when(objectPermissionService.getObjectType()).thenReturn("product");
|
||||
DemoService proxy = createProxy();
|
||||
|
||||
String result = proxy.getProductAccessible(1003L);
|
||||
|
||||
assertEquals("accessible", result);
|
||||
verify(objectPermissionService, times(1)).checkAccessible(1003L);
|
||||
// accessible 模式不应再走权限码校验
|
||||
verify(objectPermissionService, never()).checkPermission(anyLong(), anyString(), anyBoolean());
|
||||
}
|
||||
|
||||
private DemoService createProxy() {
|
||||
AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new DemoService());
|
||||
proxyFactory.addAspect(new ObjectPermissionAspect(java.util.List.of(objectPermissionService)));
|
||||
@@ -61,6 +78,11 @@ class ObjectPermissionAspectTest extends BaseMockitoUnitTest {
|
||||
return "context";
|
||||
}
|
||||
|
||||
@CheckObjectPermission(objectType = "product", objectId = "#productId", accessible = true)
|
||||
public String getProductAccessible(Long productId) {
|
||||
return "accessible";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DemoReqVO {
|
||||
|
||||
@@ -4,8 +4,12 @@ import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -20,6 +24,7 @@ import java.util.Set;
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -33,6 +38,10 @@ class ProductObjectPermissionServiceTest extends BaseMockitoUnitTest {
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Mock
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Mock
|
||||
private ProductMapper productMapper;
|
||||
@Mock
|
||||
private ObjectDataScopeService objectDataScopeService;
|
||||
|
||||
@Test
|
||||
void checkPermission_whenMemberOnlyAndCurrentUserIsMember_shouldPass() {
|
||||
@@ -77,6 +86,8 @@ class ProductObjectPermissionServiceTest extends BaseMockitoUnitTest {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkPermission(productId, "project:product:delete", false));
|
||||
assertEquals(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
// 用户可见 message 不得外泄权限码(技术细节走 log.warn)
|
||||
assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,11 +102,115 @@ class ProductObjectPermissionServiceTest extends BaseMockitoUnitTest {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkPermission(productId, "project:product:query", false));
|
||||
assertEquals(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
|
||||
}
|
||||
|
||||
verifyNoInteractions(objectPermissionApi);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkPermission_whenMemberOnlyAndNotMember_shouldThrowWithoutLeak() {
|
||||
Long productId = 1005L;
|
||||
Long loginUserId = 2005L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkPermission(productId, "project:product:query", true));
|
||||
assertEquals(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
// memberOnly 分支原先会外泄字面 member 标记,现统一为友好文案
|
||||
assertFalse(ex.getMessage().contains("member"), "message 不应外泄 member 标记");
|
||||
assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenActiveMember_shouldPass() {
|
||||
Long productId = 1101L;
|
||||
Long loginUserId = 2101L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
|
||||
.thenReturn(List.of(createMember(productId, loginUserId, 3101L)));
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
assertDoesNotThrow(() -> permissionService.checkAccessible(productId));
|
||||
}
|
||||
verifyNoInteractions(productMapper, objectDataScopeService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenImplicitObserverInScope_shouldPass() {
|
||||
Long productId = 1102L;
|
||||
Long loginUserId = 2102L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
ProductDO product = new ProductDO();
|
||||
product.setId(productId);
|
||||
product.setDirectionCode("D-NET");
|
||||
when(productMapper.selectById(productId)).thenReturn(product);
|
||||
when(objectDataScopeService.compute(loginUserId, "product"))
|
||||
.thenReturn(ObjectDataScope.idList(Collections.emptySet(), Set.of("D-NET")));
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
assertDoesNotThrow(() -> permissionService.checkAccessible(productId));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenSuperAdminScopeAll_shouldPass() {
|
||||
Long productId = 1103L;
|
||||
Long loginUserId = 2103L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
ProductDO product = new ProductDO();
|
||||
product.setId(productId);
|
||||
product.setDirectionCode("D-NET");
|
||||
when(productMapper.selectById(productId)).thenReturn(product);
|
||||
when(objectDataScopeService.compute(loginUserId, "product"))
|
||||
.thenReturn(ObjectDataScope.all());
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
assertDoesNotThrow(() -> permissionService.checkAccessible(productId));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenNeitherMemberNorScope_shouldThrowDenied() {
|
||||
Long productId = 1104L;
|
||||
Long loginUserId = 2104L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
ProductDO product = new ProductDO();
|
||||
product.setId(productId);
|
||||
product.setDirectionCode("D-NET");
|
||||
when(productMapper.selectById(productId)).thenReturn(product);
|
||||
when(objectDataScopeService.compute(loginUserId, "product"))
|
||||
.thenReturn(ObjectDataScope.empty());
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkAccessible(productId));
|
||||
assertEquals(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenObjectNotExists_shouldThrowDenied() {
|
||||
Long productId = 1105L;
|
||||
Long loginUserId = 2105L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
when(productMapper.selectById(productId)).thenReturn(null);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkAccessible(productId));
|
||||
assertEquals(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
}
|
||||
verifyNoInteractions(objectDataScopeService);
|
||||
}
|
||||
|
||||
private UserObjectRoleDO createMember(Long productId, Long loginUserId, Long roleId) {
|
||||
UserObjectRoleDO member = new UserObjectRoleDO();
|
||||
member.setId(9001L);
|
||||
|
||||
@@ -4,8 +4,12 @@ import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -20,6 +24,7 @@ import java.util.Set;
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -33,6 +38,10 @@ class ProjectObjectPermissionServiceTest extends BaseMockitoUnitTest {
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Mock
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Mock
|
||||
private ProjectMapper projectMapper;
|
||||
@Mock
|
||||
private ObjectDataScopeService objectDataScopeService;
|
||||
|
||||
@Test
|
||||
void checkPermission_whenMemberOnlyAndActiveMember_shouldPass() {
|
||||
@@ -59,6 +68,8 @@ class ProjectObjectPermissionServiceTest extends BaseMockitoUnitTest {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkPermission(projectId, "project:project:update", false));
|
||||
assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
// 用户可见 message 不得外泄权限码(技术细节走 log.warn)
|
||||
assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +102,117 @@ class ProjectObjectPermissionServiceTest extends BaseMockitoUnitTest {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkPermission(projectId, "project:project:delete", false));
|
||||
assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkPermission_whenMemberOnlyAndNoActiveMember_shouldThrowWithoutLeak() {
|
||||
Long projectId = 1005L;
|
||||
Long loginUserId = 2005L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkPermission(projectId, "project:project:query", true));
|
||||
assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
// memberOnly 分支原先会外泄字面 member 标记,现统一为友好文案
|
||||
assertFalse(ex.getMessage().contains("member"), "message 不应外泄 member 标记");
|
||||
assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenActiveMember_shouldPass() {
|
||||
Long projectId = 1101L;
|
||||
Long loginUserId = 2101L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
|
||||
.thenReturn(List.of(createMember(projectId, loginUserId, 3101L)));
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
assertDoesNotThrow(() -> permissionService.checkAccessible(projectId));
|
||||
}
|
||||
// 显式成员提前放行,不触达对象查询与数据范围计算
|
||||
verifyNoInteractions(projectMapper, objectDataScopeService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenImplicitObserverInScope_shouldPass() {
|
||||
Long projectId = 1102L;
|
||||
Long loginUserId = 2102L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
ProjectDO project = new ProjectDO();
|
||||
project.setId(projectId);
|
||||
project.setDirectionCode("D-NET");
|
||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||
// 无显式角色,但方向 scope 命中 → 隐式 observer 放行
|
||||
when(objectDataScopeService.compute(loginUserId, "project"))
|
||||
.thenReturn(ObjectDataScope.idList(Collections.emptySet(), Set.of("D-NET")));
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
assertDoesNotThrow(() -> permissionService.checkAccessible(projectId));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenSuperAdminScopeAll_shouldPass() {
|
||||
Long projectId = 1103L;
|
||||
Long loginUserId = 2103L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
ProjectDO project = new ProjectDO();
|
||||
project.setId(projectId);
|
||||
project.setDirectionCode("D-NET");
|
||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||
when(objectDataScopeService.compute(loginUserId, "project"))
|
||||
.thenReturn(ObjectDataScope.all());
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
assertDoesNotThrow(() -> permissionService.checkAccessible(projectId));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenNeitherMemberNorScope_shouldThrowDenied() {
|
||||
Long projectId = 1104L;
|
||||
Long loginUserId = 2104L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
ProjectDO project = new ProjectDO();
|
||||
project.setId(projectId);
|
||||
project.setDirectionCode("D-NET");
|
||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||
when(objectDataScopeService.compute(loginUserId, "project"))
|
||||
.thenReturn(ObjectDataScope.empty());
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkAccessible(projectId));
|
||||
assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAccessible_whenObjectNotExists_shouldThrowDenied() {
|
||||
Long projectId = 1105L;
|
||||
Long loginUserId = 2105L;
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
|
||||
.thenReturn(Collections.emptyList());
|
||||
when(projectMapper.selectById(projectId)).thenReturn(null);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> permissionService.checkAccessible(projectId));
|
||||
// spec §3.3 定稿:对象不存在也返回 DENIED,不暴露存在性
|
||||
assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
|
||||
}
|
||||
// 对象不存在即终止,不再计算数据范围
|
||||
verifyNoInteractions(objectDataScopeService);
|
||||
}
|
||||
|
||||
private UserObjectRoleDO createMember(Long projectId, Long loginUserId, Long roleId) {
|
||||
UserObjectRoleDO member = new UserObjectRoleDO();
|
||||
member.setId(9001L);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.njcn.rdms.module.project.service.overtime;
|
||||
|
||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationStatusLogMapper;
|
||||
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.service.status.StatusActionTextResolver;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 加班申请状态机错误文案单测(TD-012)。
|
||||
* 验证:状态机「动作不允许 / 缺原因」抛错时,message 填的是 {@link StatusActionTextResolver} 翻出的中文名,
|
||||
* 不外泄英文动作 / 状态 code(技术 token 只进 log,不进用户可见 message)。
|
||||
* resolver 自身的 DB 翻译由 StatusActionTextResolverTest 覆盖,这里只验证 service 填进 message 的是中文名。
|
||||
*/
|
||||
class OvertimeApplicationServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private OvertimeApplicationServiceImpl overtimeApplicationService;
|
||||
|
||||
@Mock
|
||||
private OvertimeApplicationMapper overtimeApplicationMapper;
|
||||
@Mock
|
||||
private OvertimeApplicationStatusLogMapper overtimeApplicationStatusLogMapper;
|
||||
@Mock
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@Mock
|
||||
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||
@Mock
|
||||
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
||||
@Mock
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
@Mock
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
private static final String OBJECT_TYPE = OvertimeApplicationConstants.STATUS_OBJECT_TYPE;
|
||||
|
||||
@Test
|
||||
void approve_whenTransitionNotAllowed_shouldThrowWithChineseNameNoCodeLeak() {
|
||||
Long id = 1001L;
|
||||
Long loginUserId = 2001L;
|
||||
OvertimeApplicationDO application = new OvertimeApplicationDO();
|
||||
application.setId(id);
|
||||
application.setApproverId(loginUserId);
|
||||
application.setStatusCode(OvertimeApplicationConstants.STATUS_APPROVED);
|
||||
when(overtimeApplicationMapper.selectById(id)).thenReturn(application);
|
||||
// 当前状态 + 动作在状态机表查不到流转 → 不允许
|
||||
when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
OBJECT_TYPE, OvertimeApplicationConstants.STATUS_APPROVED, OvertimeApplicationConstants.ACTION_APPROVE))
|
||||
.thenReturn(null);
|
||||
when(statusActionTextResolver.statusName(OBJECT_TYPE, OvertimeApplicationConstants.STATUS_APPROVED))
|
||||
.thenReturn("已通过");
|
||||
when(statusActionTextResolver.actionName(OBJECT_TYPE, OvertimeApplicationConstants.ACTION_APPROVE))
|
||||
.thenReturn("通过");
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockLoginUser(loginUserId)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> overtimeApplicationService.approve(id, new OvertimeApplicationStatusActionReqVO()));
|
||||
assertEquals(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED.getCode(), ex.getCode());
|
||||
// message 用中文名
|
||||
assertTrue(ex.getMessage().contains("已通过"), "message 应含状态中文名");
|
||||
assertTrue(ex.getMessage().contains("通过"), "message 应含动作中文名");
|
||||
// 不外泄英文动作 / 状态 code
|
||||
assertFalse(ex.getMessage().contains(OvertimeApplicationConstants.ACTION_APPROVE), "message 不应外泄英文动作 code");
|
||||
assertFalse(ex.getMessage().contains(OvertimeApplicationConstants.STATUS_APPROVED), "message 不应外泄英文状态 code");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void approve_whenReasonRequiredButBlank_shouldThrowWithChineseActionName() {
|
||||
Long id = 1002L;
|
||||
Long loginUserId = 2002L;
|
||||
OvertimeApplicationDO application = new OvertimeApplicationDO();
|
||||
application.setId(id);
|
||||
application.setApproverId(loginUserId);
|
||||
application.setStatusCode(OvertimeApplicationConstants.STATUS_PENDING);
|
||||
when(overtimeApplicationMapper.selectById(id)).thenReturn(application);
|
||||
// 流转存在但要求填原因,而入参未带原因 → REASON_REQUIRED
|
||||
ObjectStatusTransitionDO transition = new ObjectStatusTransitionDO();
|
||||
transition.setNeedReason(Boolean.TRUE);
|
||||
when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
OBJECT_TYPE, OvertimeApplicationConstants.STATUS_PENDING, OvertimeApplicationConstants.ACTION_APPROVE))
|
||||
.thenReturn(transition);
|
||||
when(statusActionTextResolver.actionName(OBJECT_TYPE, OvertimeApplicationConstants.ACTION_APPROVE))
|
||||
.thenReturn("通过");
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockLoginUser(loginUserId)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> overtimeApplicationService.approve(id, new OvertimeApplicationStatusActionReqVO()));
|
||||
assertEquals(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED.getCode(), ex.getCode());
|
||||
assertTrue(ex.getMessage().contains("通过"), "message 应含动作中文名");
|
||||
assertFalse(ex.getMessage().contains(OvertimeApplicationConstants.ACTION_APPROVE), "message 不应外泄英文动作 code");
|
||||
}
|
||||
}
|
||||
|
||||
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId) {
|
||||
MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class);
|
||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||
return mocked;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,8 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
private ObjectStatusTransitionMapper statusTransitionMapper;
|
||||
@Mock
|
||||
private ObjectStatusModelMapper statusModelMapper;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
// ========== 创建需求测试 ==========
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ class ProductServiceImplTest extends BaseMockitoUnitTest {
|
||||
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
||||
@Mock
|
||||
private ObjectDataScopeService objectDataScopeService;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Test
|
||||
void createProduct_shouldCreateDefaultRequirementModule() {
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package com.njcn.rdms.module.project.service.project;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
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.project.vo.myproject.MyProjectPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectParticipatedRespVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||
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.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MyProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private MyProjectServiceImpl myProjectService;
|
||||
@Mock
|
||||
private ProjectMapper projectMapper;
|
||||
@Mock
|
||||
private ProjectTaskMapper projectTaskMapper;
|
||||
@Mock
|
||||
private ProjectExecutionMapper projectExecutionMapper;
|
||||
@Mock
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Mock
|
||||
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||
@Mock
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Mock
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
private MyProjectPageReqVO allPageReq() {
|
||||
MyProjectPageReqVO reqVO = new MyProjectPageReqVO();
|
||||
reqVO.setPageNo(1);
|
||||
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
return reqVO;
|
||||
}
|
||||
|
||||
private UserObjectRoleDO role(Long id, Long objectId, Long roleId) {
|
||||
UserObjectRoleDO r = new UserObjectRoleDO();
|
||||
r.setId(id);
|
||||
r.setObjectType(ProjectObjectConstants.OBJECT_TYPE);
|
||||
r.setObjectId(objectId);
|
||||
r.setRoleId(roleId);
|
||||
r.setStatus(0);
|
||||
return r;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParticipated_emptyWhenNoRoles() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> mock = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mock.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(100L);
|
||||
when(userObjectRoleMapper.selectActiveListByObjectTypeAndUserId(
|
||||
eq(ProjectObjectConstants.OBJECT_TYPE), eq(100L))).thenReturn(emptyList());
|
||||
|
||||
PageResult<MyProjectParticipatedRespVO> result = myProjectService.getMyParticipatedPage(allPageReq());
|
||||
|
||||
assertEquals(0L, result.getTotal());
|
||||
assertEquals(0, result.getList().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParticipated_assemblesRoleProgressAndTaskCount() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> mock = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mock.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(100L);
|
||||
// 我在项目 2001 有两个角色行:roleId=10(manager) + roleId=20(dev)
|
||||
when(userObjectRoleMapper.selectActiveListByObjectTypeAndUserId(
|
||||
eq(ProjectObjectConstants.OBJECT_TYPE), eq(100L)))
|
||||
.thenReturn(List.of(role(1L, 2001L, 10L), role(2L, 2001L, 20L)));
|
||||
ProjectDO project = new ProjectDO();
|
||||
project.setId(2001L);
|
||||
project.setProjectName("商城 V2 升级");
|
||||
project.setProjectCode("MALL-V2");
|
||||
project.setStatusCode("active");
|
||||
project.setProgressRate(new BigDecimal("69.6"));
|
||||
when(projectMapper.selectBatchIds(anyCollection())).thenReturn(List.of(project));
|
||||
// statusName 回填
|
||||
ObjectStatusModelDO sm = new ObjectStatusModelDO();
|
||||
sm.setStatusCode("active");
|
||||
sm.setStatusName("进行中");
|
||||
when(objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE))
|
||||
.thenReturn(List.of(sm));
|
||||
// 角色名:10=项目负责人(manager),20=开发
|
||||
ObjectRoleRespDTO manager = new ObjectRoleRespDTO();
|
||||
manager.setId(10L);
|
||||
manager.setCode(ProjectObjectConstants.MANAGER_ROLE_CODE);
|
||||
manager.setName("项目负责人");
|
||||
ObjectRoleRespDTO dev = new ObjectRoleRespDTO();
|
||||
dev.setId(20L);
|
||||
dev.setCode("dev");
|
||||
dev.setName("开发");
|
||||
when(objectPermissionApi.getObjectRoleList(anyCollection(), any(), any()))
|
||||
.thenReturn(success(List.of(manager, dev)));
|
||||
// 终态集 + 任务计数
|
||||
when(objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE))
|
||||
.thenReturn(List.of("completed", "cancelled"));
|
||||
when(projectTaskMapper.selectMyTaskCountGroupByProjectIds(eq(100L), anyCollection(), anyCollection()))
|
||||
.thenReturn(List.<Map<String, Object>>of(Map.of(
|
||||
"projectId", 2001L, "totalCount", 8L, "pendingCount", 3L)));
|
||||
|
||||
PageResult<MyProjectParticipatedRespVO> result = myProjectService.getMyParticipatedPage(allPageReq());
|
||||
|
||||
assertEquals(1L, result.getTotal());
|
||||
MyProjectParticipatedRespVO vo = result.getList().get(0);
|
||||
assertEquals(2001L, vo.getId());
|
||||
assertEquals("MALL-V2", vo.getCode());
|
||||
assertEquals("进行中", vo.getStatusName());
|
||||
assertEquals(70, vo.getProgress()); // 69.6 → HALF_UP → 70
|
||||
assertEquals("项目负责人 / 开发", vo.getMyRole()); // manager 主 + 附加
|
||||
assertEquals(8, vo.getMyTaskCount());
|
||||
assertEquals(3, vo.getMyPendingTaskCount());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParticipated_excludesProjectWhenOnlyInvisibleRole() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> mock = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mock.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(100L);
|
||||
// 我在项目 2001 只有一个隐式角色 roleId=90(visible=0,如创建者),无任何可见角色
|
||||
when(userObjectRoleMapper.selectActiveListByObjectTypeAndUserId(
|
||||
eq(ProjectObjectConstants.OBJECT_TYPE), eq(100L)))
|
||||
.thenReturn(List.of(role(1L, 2001L, 90L)));
|
||||
ProjectDO project = new ProjectDO();
|
||||
project.setId(2001L);
|
||||
project.setProjectName("商城 V2 升级");
|
||||
project.setProjectCode("MALL-V2");
|
||||
project.setStatusCode("active");
|
||||
when(projectMapper.selectBatchIds(anyCollection())).thenReturn(List.of(project));
|
||||
when(objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE))
|
||||
.thenReturn(emptyList());
|
||||
// 角色 90:创建者,visible=0
|
||||
ObjectRoleRespDTO creator = new ObjectRoleRespDTO();
|
||||
creator.setId(90L);
|
||||
creator.setCode("project_creator");
|
||||
creator.setName("创建者");
|
||||
creator.setVisible(0);
|
||||
when(objectPermissionApi.getObjectRoleList(anyCollection(), any(), any()))
|
||||
.thenReturn(success(List.of(creator)));
|
||||
when(objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE))
|
||||
.thenReturn(List.of("completed", "cancelled"));
|
||||
when(projectTaskMapper.selectMyTaskCountGroupByProjectIds(eq(100L), anyCollection(), anyCollection()))
|
||||
.thenReturn(emptyList());
|
||||
|
||||
PageResult<MyProjectParticipatedRespVO> result = myProjectService.getMyParticipatedPage(allPageReq());
|
||||
|
||||
// 仅隐式角色 → 该项目整项剔除
|
||||
assertEquals(0L, result.getTotal());
|
||||
assertEquals(0, result.getList().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOwned_emptyWhenNoManagedProjects() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> mock = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mock.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(100L);
|
||||
when(projectMapper.selectList(any(com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX.class)))
|
||||
.thenReturn(emptyList());
|
||||
|
||||
PageResult<com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectOwnedRespVO>
|
||||
result = myProjectService.getMyOwnedPage(allPageReq());
|
||||
|
||||
assertEquals(0L, result.getTotal());
|
||||
assertEquals(0, result.getList().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOwned_assemblesCountsAndMembers() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> mock = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mock.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(100L);
|
||||
ProjectDO project = new ProjectDO();
|
||||
project.setId(2001L);
|
||||
project.setProjectName("商城 V2 升级");
|
||||
project.setProjectCode("MALL-V2");
|
||||
project.setProgressRate(new BigDecimal("70.0"));
|
||||
when(projectMapper.selectList(any(com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX.class)))
|
||||
.thenReturn(List.of(project));
|
||||
when(objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE))
|
||||
.thenReturn(List.of("completed", "cancelled"));
|
||||
when(objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(
|
||||
com.njcn.rdms.module.project.constant.ProjectExecutionConstants.OBJECT_TYPE))
|
||||
.thenReturn(List.of("completed", "cancelled"));
|
||||
when(projectTaskMapper.selectTaskAndOverdueCountGroupByProjectIds(anyCollection(), anyCollection(), any()))
|
||||
.thenReturn(List.<Map<String, Object>>of(Map.of(
|
||||
"projectId", 2001L, "taskCount", 24L, "overdueCount", 2L)));
|
||||
when(projectExecutionMapper.selectExecutionCountGroupByProjectIds(anyCollection(), anyCollection()))
|
||||
.thenReturn(List.<Map<String, Object>>of(Map.of("projectId", 2001L, "executionCount", 6L)));
|
||||
when(projectTaskMapper.selectActiveTaskCountGroupByProjectIdAndOwner(anyCollection(), anyCollection()))
|
||||
.thenReturn(List.<Map<String, Object>>of(
|
||||
Map.of("projectId", 2001L, "ownerId", 101L, "activeTaskCount", 6L),
|
||||
Map.of("projectId", 2001L, "ownerId", 102L, "activeTaskCount", 3L)));
|
||||
// 成员清单:101 / 102(102 出现两行多角色,去重后仍一个成员)
|
||||
UserObjectRoleDO m1 = role(11L, 2001L, 10L);
|
||||
m1.setUserId(101L);
|
||||
UserObjectRoleDO m2 = role(12L, 2001L, 20L);
|
||||
m2.setUserId(102L);
|
||||
UserObjectRoleDO m3 = role(13L, 2001L, 30L);
|
||||
m3.setUserId(102L);
|
||||
when(userObjectRoleMapper.selectActiveListByObjectTypeAndObjectIds(
|
||||
eq(ProjectObjectConstants.OBJECT_TYPE), anyCollection()))
|
||||
.thenReturn(List.of(m1, m2, m3));
|
||||
com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO u101 =
|
||||
new com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO();
|
||||
u101.setId(101L);
|
||||
u101.setNickname("张三");
|
||||
com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO u102 =
|
||||
new com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO();
|
||||
u102.setId(102L);
|
||||
u102.setNickname("李四");
|
||||
when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(101L, u101, 102L, u102));
|
||||
ObjectRoleRespDTO manager = new ObjectRoleRespDTO();
|
||||
manager.setName("项目负责人");
|
||||
when(objectPermissionApi.getObjectRoleByCode(eq(ProjectObjectConstants.MANAGER_ROLE_CODE), any(), any()))
|
||||
.thenReturn(success(manager));
|
||||
|
||||
PageResult<com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectOwnedRespVO>
|
||||
result = myProjectService.getMyOwnedPage(allPageReq());
|
||||
|
||||
assertEquals(1L, result.getTotal());
|
||||
var vo = result.getList().get(0);
|
||||
assertEquals(70, vo.getProgress());
|
||||
assertEquals("项目负责人", vo.getMyRole());
|
||||
assertEquals(24, vo.getTaskCount());
|
||||
assertEquals(2, vo.getOverdueCount());
|
||||
assertEquals(6, vo.getExecutionCount());
|
||||
assertEquals(2, vo.getMemberCount()); // 101 + 102 去重
|
||||
assertEquals(2, vo.getMembers().size());
|
||||
assertEquals(6, vo.getMembers().get(0).getActiveTaskCount()); // 101 → 6
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,8 @@ class ProjectRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||
@Mock
|
||||
private ProjectExecutionMapper projectExecutionMapper;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Test
|
||||
void validateUsableForExecution_whenRequirementIdIsNull_shouldDoNothing() {
|
||||
|
||||
@@ -89,6 +89,8 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest {
|
||||
private DictDataApi dictDataApi;
|
||||
@Mock
|
||||
private ObjectDataScopeService objectDataScopeService;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Test
|
||||
void getProjectDetail_shouldFillProductNameAndManagerNickname() {
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
|
||||
@@ -101,6 +103,8 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
private ProjectRequirementService projectRequirementService;
|
||||
@Mock
|
||||
private ProjectRequirementMapper projectRequirementMapper;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
/**
|
||||
* 默认让 dictDataApi.validateDictDataList 对 REQ_PRIORITY 返回 true,既有测试不因 priority 校验失败。
|
||||
@@ -421,11 +425,16 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||
when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("execution", "active", "complete"))
|
||||
.thenReturn(null);
|
||||
when(statusActionTextResolver.statusName("execution", "active")).thenReturn("进行中");
|
||||
when(statusActionTextResolver.actionName("execution", "complete")).thenReturn("完成");
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3001L)) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> projectExecutionService.changeExecutionStatus(projectId, executionId, reqVO));
|
||||
assertEquals(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED.getCode(), ex.getCode());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(ex.getMessage().contains("进行中"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(ex.getMessage().contains("完成"));
|
||||
org.junit.jupiter.api.Assertions.assertFalse(ex.getMessage().contains("complete"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,6 +649,88 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMyExecutionPage_shouldExcludeTerminalAndFullProgressAndAssembleProjectName() {
|
||||
Long loginUserId = 3001L;
|
||||
Long projectId = 2001L;
|
||||
// 5001 active 进度 25.555 → 保留(26);5002 completed 终态 → 排除;
|
||||
// 5003 active 进度 100 → 排除;5004 active 无根任务 → 进度 0 → 保留
|
||||
ProjectExecutionDO e1 = createExecution(projectId, 5001L, loginUserId);
|
||||
e1.setStatusCode("active");
|
||||
ProjectExecutionDO e2 = createExecution(projectId, 5002L, loginUserId);
|
||||
e2.setStatusCode("completed");
|
||||
ProjectExecutionDO e3 = createExecution(projectId, 5003L, loginUserId);
|
||||
e3.setStatusCode("active");
|
||||
ProjectExecutionDO e4 = createExecution(projectId, 5004L, loginUserId);
|
||||
e4.setStatusCode("active");
|
||||
|
||||
when(projectExecutionMapper.selectListByOwnerId(loginUserId))
|
||||
.thenReturn(List.of(e1, e2, e3, e4));
|
||||
// 执行域终态
|
||||
when(objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled("execution"))
|
||||
.thenReturn(List.of("completed", "cancelled"));
|
||||
// 任务域进度排除状态(loadProgressExcludedTaskStatusCodes 内部按 "task" 查)
|
||||
when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task"))
|
||||
.thenReturn(List.of("cancelled"));
|
||||
// 聚合进度:5001=25.555, 5003=100;5004 缺失 → 0
|
||||
when(projectTaskMapper.selectRootTaskAvgProgressGroupByExecutionIds(eq(projectId), anyCollection(), eq(List.of("cancelled"))))
|
||||
.thenReturn(List.of(
|
||||
Map.of("execution_id", 5001L, "progress_rate", new BigDecimal("25.555")),
|
||||
Map.of("execution_id", 5003L, "progress_rate", new BigDecimal("100.00"))));
|
||||
// 执行域状态模型(建 statusCode -> statusName)
|
||||
ObjectStatusModelDO activeStatus = new ObjectStatusModelDO();
|
||||
activeStatus.setObjectType("execution");
|
||||
activeStatus.setStatusCode("active");
|
||||
activeStatus.setStatusName("进行中");
|
||||
when(objectStatusModelMapper.selectListByObjectTypeEnabled("execution"))
|
||||
.thenReturn(List.of(activeStatus));
|
||||
// 项目名回填
|
||||
when(projectMapper.selectBatchIds(anyCollection()))
|
||||
.thenReturn(List.of(createEditableProject(projectId)));
|
||||
|
||||
MyProjectExecutionPageReqVO reqVO = new MyProjectExecutionPageReqVO();
|
||||
reqVO.setPageNo(1);
|
||||
reqVO.setPageSize(-1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||
|
||||
PageResult<MyProjectExecutionRespVO> result = projectExecutionService.getMyExecutionPage(reqVO);
|
||||
|
||||
// 仅 5001、5004 保留
|
||||
assertEquals(2L, result.getTotal());
|
||||
assertEquals(2, result.getList().size());
|
||||
assertEquals(List.of(5001L, 5004L),
|
||||
result.getList().stream().map(MyProjectExecutionRespVO::getId).collect(java.util.stream.Collectors.toList()));
|
||||
// 进度:25.555 → 四舍五入 26;无根任务 → 0
|
||||
assertEquals(Integer.valueOf(26), result.getList().get(0).getProgressRate());
|
||||
assertEquals(Integer.valueOf(0), result.getList().get(1).getProgressRate());
|
||||
// projectName / statusName 回填
|
||||
assertEquals("测试项目", result.getList().get(0).getProjectName());
|
||||
assertEquals("进行中", result.getList().get(0).getStatusName());
|
||||
assertEquals("active", result.getList().get(0).getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMyExecutionPage_whenNoExecutions_shouldReturnEmptyPage() {
|
||||
Long loginUserId = 3001L;
|
||||
when(projectExecutionMapper.selectListByOwnerId(loginUserId)).thenReturn(List.of());
|
||||
|
||||
MyProjectExecutionPageReqVO reqVO = new MyProjectExecutionPageReqVO();
|
||||
reqVO.setPageNo(1);
|
||||
reqVO.setPageSize(-1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||
|
||||
PageResult<MyProjectExecutionRespVO> result = projectExecutionService.getMyExecutionPage(reqVO);
|
||||
|
||||
assertEquals(0L, result.getTotal());
|
||||
assertEquals(0, result.getList().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getExecutionPage_shouldDelegateMapper() {
|
||||
Long projectId = 2001L;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.njcn.rdms.module.project.service.status;
|
||||
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class StatusActionTextResolverTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private StatusActionTextResolver resolver;
|
||||
@Mock
|
||||
private ObjectStatusTransitionMapper transitionMapper;
|
||||
@Mock
|
||||
private ObjectStatusModelMapper statusModelMapper;
|
||||
|
||||
@Test
|
||||
void actionName_hit_returnsChineseName() {
|
||||
when(transitionMapper.selectActionNameByObjectTypeAndAction("task", "complete")).thenReturn("完成");
|
||||
assertEquals("完成", resolver.actionName("task", "complete"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void actionName_miss_fallsBackToCode() {
|
||||
when(transitionMapper.selectActionNameByObjectTypeAndAction("task", "weird")).thenReturn(null);
|
||||
assertEquals("weird", resolver.actionName("task", "weird"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void actionName_blankInput_returnsInput() {
|
||||
assertEquals("", resolver.actionName("task", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusName_hit_returnsChineseName() {
|
||||
ObjectStatusModelDO model = new ObjectStatusModelDO();
|
||||
model.setStatusName("已完成");
|
||||
when(statusModelMapper.selectByObjectTypeAndStatusCode("task", "completed")).thenReturn(model);
|
||||
assertEquals("已完成", resolver.statusName("task", "completed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusName_miss_fallsBackToCode() {
|
||||
when(statusModelMapper.selectByObjectTypeAndStatusCode("task", "ghost")).thenReturn(null);
|
||||
assertEquals("ghost", resolver.statusName("task", "ghost"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user