From 220dec9b6c0029a5505c245abd47bbef2a288a5c Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Tue, 12 May 2026 21:18:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(file):=20=E6=94=B9=E9=80=A0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 POST /system/file/upload 接口返回结构从 string 改为 { id: string, url: string } - id 字段以字符串形式返回 infra_file.id,避免 JavaScript 数值精度丢失问题 - 保持接口路径、方法和入参完全不变,仅修改返回格式 - 添加 GET /system/file/download 接口用于文件下载功能 - 优化 AppFileController 中的文件上传实现逻辑 - 更新 AuthConvert 和 AuthUserInfoRespVO 添加用户昵称和头像字段 - 在 CLAUDE.md 中补充鉴权通道和 HTTP 动词语义说明文档 - 在 ErrorCodeConstants.java 中添加多个项目管理和执行相关的错误码定义 - 删除执行成员相关的数据库表和接口定义(执行协办人替代方案) - 在 FileMapper 中增加按 URL 查询文件的方法支持 --- .claude/settings.local.json | 13 +- AGENTS.md | 2 + CLAUDE.md | 36 +- .../rdms/framework/common/pojo/PageParam.java | 3 +- .../project/enums/ErrorCodeConstants.java | 41 +- .../constant/ObjectActivityConstants.java | 45 +- .../constant/ProjectExecutionConstants.java | 18 +- .../constant/ProjectObjectConstants.java | 3 +- .../constant/ProjectTaskConstants.java | 16 + .../admin/product/ProductController.java | 8 + .../product/ProductCreateWithTeamReqVO.java | 42 ++ .../admin/project/ProjectController.java | 8 + .../ProjectExecutionAssigneeController.java | 70 +++ .../execution/ProjectExecutionController.java | 17 +- .../ProjectExecutionMemberController.java | 70 --- .../ExecutionAssigneeInactiveReqVO.java} | 6 +- .../ExecutionAssigneeLogPageReqVO.java} | 8 +- .../ExecutionAssigneeLogRespVO.java} | 6 +- .../ExecutionAssigneeRespVO.java} | 12 +- .../assignee/ExecutionAssigneeSaveReqVO.java | 15 + ....java => ProjectExecutionCreateReqVO.java} | 17 +- .../ProjectExecutionDeleteReqVO.java | 31 ++ .../vo/execution/ProjectExecutionRespVO.java | 2 +- .../ProjectExecutionUpdateReqVO.java | 47 ++ .../vo/member/ExecutionMemberSaveReqVO.java | 15 - .../project/task/ProjectTaskController.java | 11 + .../project/task/TaskAssigneeController.java | 3 - .../project/task/TaskWorklogController.java | 4 - .../task/vo/ProjectTaskDeleteReqVO.java | 31 ++ .../project/task/vo/ProjectTaskRespVO.java | 17 +- .../project/task/vo/ProjectTaskSaveReqVO.java | 14 +- .../task/vo/ProjectTaskStatusActionReqVO.java | 2 +- .../task/vo/worklog/TaskWorklogPageReqVO.java | 4 +- .../task/vo/worklog/TaskWorklogRespVO.java | 20 +- .../task/vo/worklog/TaskWorklogSaveReqVO.java | 44 +- .../project/ProjectCreateWithTeamReqVO.java | 43 ++ .../dataobject/attachment/AttachmentItem.java | 37 ++ ...MemberDO.java => ExecutionAssigneeDO.java} | 8 +- ...LogDO.java => ExecutionAssigneeLogDO.java} | 8 +- .../project/task/ProjectTaskDO.java | 12 +- .../project/task/TaskWorklogDO.java | 44 +- .../execution/ExecutionAssigneeLogMapper.java | 30 + .../execution/ExecutionAssigneeMapper.java | 70 +++ .../execution/ExecutionMemberLogMapper.java | 30 - .../execution/ExecutionMemberMapper.java | 51 -- .../execution/ProjectExecutionMapper.java | 50 +- .../mysql/project/task/ProjectTaskMapper.java | 129 ++++- .../project/task/TaskAssigneeMapper.java | 37 ++ .../mysql/project/task/TaskWorklogMapper.java | 110 +++- .../attachment/AttachmentFileIdResolver.java | 40 ++ .../attachment/AttachmentValidator.java | 98 ++++ .../rpc/config/RpcConfiguration.java | 3 +- .../ProjectObjectAuthorizationService.java | 29 + .../service/product/ProductService.java | 17 + .../service/product/ProductServiceImpl.java | 105 ++++ .../service/project/ProjectService.java | 21 + .../service/project/ProjectServiceImpl.java | 156 +++++- .../ProjectStatusBoardServiceImpl.java | 34 +- .../ProjectExecutionAssigneeService.java | 49 ++ ... ProjectExecutionAssigneeServiceImpl.java} | 202 +++---- .../ProjectExecutionMemberService.java | 49 -- .../execution/ProjectExecutionService.java | 25 +- .../ProjectExecutionServiceImpl.java | 435 +++++++++++++-- .../ProjectExecutionStatusViewService.java | 50 +- .../project/permission/VisibilityScope.java | 31 ++ .../permission/VisibilityScopeResolver.java | 28 + .../VisibilityScopeResolverImpl.java | 76 +++ .../project/task/ProjectTaskService.java | 51 ++ .../project/task/ProjectTaskServiceImpl.java | 514 ++++++++++++++++-- .../task/ProjectTaskStatusViewService.java | 49 +- .../assignee/TaskAssigneeServiceImpl.java | 11 +- .../task/worklog/TaskWorklogService.java | 33 +- .../task/worklog/TaskWorklogServiceImpl.java | 168 +++++- .../AttachmentFileIdResolverTest.java | 52 ++ ...ProjectObjectAuthorizationServiceTest.java | 51 ++ .../ProjectStatusBoardServiceTest.java | 49 +- ...jectExecutionAssigneeServiceImplTest.java} | 172 +++--- .../ProjectExecutionServiceImplTest.java | 179 +++++- ...ProjectExecutionStatusViewServiceTest.java | 111 +++- .../VisibilityScopeResolverImplTest.java | 126 +++++ .../task/ProjectTaskServiceImplTest.java | 219 +++----- .../ProjectTaskStatusViewServiceTest.java | 95 +++- .../assignee/TaskAssigneeServiceImplTest.java | 30 +- .../worklog/TaskWorklogServiceImplTest.java | 412 -------------- .../2026-05-11-file-upload-api-改造需求.md | 98 ++++ .../rdms/module/system/api/file/FileApi.java | 24 + .../system/api/file/dto/FileRespDTO.java | 38 ++ .../module/system/api/file/FileApiImpl.java | 29 + .../admin/auth/vo/AuthUserInfoRespVO.java | 6 + .../controller/admin/file/FileController.java | 22 +- .../admin/file/vo/file/FileUploadRespVO.java | 20 + .../app/file/AppFileController.java | 6 +- .../system/convert/auth/AuthConvert.java | 2 + .../system/dal/mysql/file/FileMapper.java | 18 + .../system/service/file/FileService.java | 14 +- .../system/service/file/FileServiceImpl.java | 17 +- .../admin/file/FileControllerTest.java | 46 ++ .../system/dal/mysql/file/FileMapperTest.java | 30 + 98 files changed, 4138 insertions(+), 1362 deletions(-) create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductCreateWithTeamReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionAssigneeController.java delete mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionMemberController.java rename rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/{member/ExecutionMemberInactiveReqVO.java => assignee/ExecutionAssigneeInactiveReqVO.java} (78%) rename rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/{member/ExecutionMemberLogPageReqVO.java => assignee/ExecutionAssigneeLogPageReqVO.java} (76%) rename rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/{member/ExecutionMemberLogRespVO.java => assignee/ExecutionAssigneeLogRespVO.java} (88%) rename rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/{member/ExecutionMemberRespVO.java => assignee/ExecutionAssigneeRespVO.java} (62%) create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeSaveReqVO.java rename rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/{ProjectExecutionSaveReqVO.java => ProjectExecutionCreateReqVO.java} (77%) create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionDeleteReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionUpdateReqVO.java delete mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberSaveReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskDeleteReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/project/ProjectCreateWithTeamReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/attachment/AttachmentItem.java rename rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/{ExecutionMemberDO.java => ExecutionAssigneeDO.java} (84%) rename rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/{ExecutionMemberLogDO.java => ExecutionAssigneeLogDO.java} (87%) create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeLogMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeMapper.java delete mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionMemberLogMapper.java delete mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionMemberMapper.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/attachment/AttachmentFileIdResolver.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/attachment/AttachmentValidator.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectAuthorizationService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionAssigneeService.java rename rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/{ProjectExecutionMemberServiceImpl.java => ProjectExecutionAssigneeServiceImpl.java} (64%) delete mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionMemberService.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScope.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolver.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolverImpl.java create mode 100644 rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/attachment/AttachmentFileIdResolverTest.java create mode 100644 rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/security/service/ProjectObjectAuthorizationServiceTest.java rename rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/{ProjectExecutionMemberServiceImplTest.java => ProjectExecutionAssigneeServiceImplTest.java} (63%) create mode 100644 rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/permission/VisibilityScopeResolverImplTest.java delete mode 100644 rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImplTest.java create mode 100644 rdms-system/2026-05-11-file-upload-api-改造需求.md create mode 100644 rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java create mode 100644 rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/dto/FileRespDTO.java create mode 100644 rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java create mode 100644 rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/vo/file/FileUploadRespVO.java create mode 100644 rdms-system/rdms-system-boot/src/test/java/com/njcn/rdms/module/system/controller/admin/file/FileControllerTest.java create mode 100644 rdms-system/rdms-system-boot/src/test/java/com/njcn/rdms/module/system/dal/mysql/file/FileMapperTest.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bbb4ad5..0d830a9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,18 @@ "PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am test 2>&1 | Select-Object -Last 80)", "PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot \"-Dtest=TaskAssigneeServiceImplTest,ProjectTaskServiceImplTest,ProjectTaskStatusViewServiceTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 30)", "PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am \"-Dtest=TaskAssigneeServiceImplTest,ProjectTaskServiceImplTest,ProjectTaskStatusViewServiceTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 30)", - "Bash(grep -rn \"INSERT INTO \\\\`system_menu\\\\`\\\\|INSERT INTO system_menu\" --include=\"*.sql\" rdms-project rdms-system)" + "Bash(grep -rn \"INSERT INTO \\\\`system_menu\\\\`\\\\|INSERT INTO system_menu\" --include=\"*.sql\" rdms-project rdms-system)", + "Bash(findstr /i \"plugin\")", + "Bash(findstr *)", + "Bash(awk END{print NR} *)", + "PowerShell(Move-Item *)", + "PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dtest=VisibilityScopeResolverImplTest' -q 2>&1 | Select-Object -Last 80)", + "PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' -q 2>&1 | Select-Object -Last 60)", + "PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' -q | Select-Object -Last 60)", + "PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|ERROR|FAIL' | Select-Object -Last 40)", + "PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|ERROR|FAILED|FAIL' | Select-Object -Last 100)", + "PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test-compile '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'ERROR|BUILD|FAIL' | Select-Object -Last 40)", + "PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot test '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|FAILED|ERROR' | Select-Object -Last 80)" ] } } diff --git a/AGENTS.md b/AGENTS.md index b6b62fa..0d4978c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,8 @@ 默认回答保持精简,优先给结论、改动点和必要风险,不做过多展开;如果存在你关心但未展开的细节,由你继续追问后再补充。 +回答问题时不要过多代码层面的描述:默认用自然语言给结论、判断、影响面;除非用户明确要看实现细节,不要大段贴代码片段、不要逐行解读、不要把分析写成"先看 xxx.java 第 N 行"的形式。涉及代码定位时用 `file_path:line_number` 引用即可。 + ## 交互原则 - 默认先给执行方案,说明目标、涉及模块、预计改动点和验证方式。 diff --git a/CLAUDE.md b/CLAUDE.md index b683703..9544a81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ - 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。 - 回答保持精简,先给结论、改动点、必要风险;细节等用户追问。 - **不要废话**:默认极简输出,不展开背景、不复述需求、不堆叠章节标题;能用一两句讲清就别写成清单;用户主动追问再展开。 +- **回答问题时不要过多代码层面的描述**:默认用自然语言给结论、判断、影响面;除非用户明确要看实现细节,不要大段贴代码片段、不要逐行解读、不要把分析写成"先看 xxx.java 第 N 行"的形式。涉及代码定位时用 `file_path:line_number` 引用即可。 ## 本机环境 @@ -64,9 +65,42 @@ ## 认证与跨模块调用 - 默认沿用 OAuth2 / Token / `LoginUser` / `login-user` 透传主链。**不要**另造 ThreadLocal / Session / 自定义 header。 -- 接口级权限走 `@PreAuthorize("@ss.hasPermission(...)")`,不要绕开。 - 跨模块/跨服务必须通过 `*-api` 模块定义契约;不要直接依赖别人的 `*-boot`。 +### 鉴权:必须按"全域 / 对象域"分通道挂 + +系统有**两条互不交叉**的权限通道,挂错通道 = 永远 403。新增/修改接口前必须先判断它属于哪一域: + +| 通道 | 适用场景 | 注解 / 实现 | 角色与菜单 | +|---|---|---|---| +| **全域 global** | 传统 RBAC 顶层菜单与"项目管理界面"——选择对象**之前**的所有动作(建项目、列项目、菜单/角色/用户管理等) | Controller 上 `@PreAuthorize("@ss.hasPermission('xxx')")`,由 `PermissionServiceImpl.hasAnyPermissions` 处理 | `system_role.scope_type='global'` + `system_menu.scope_type='global'` | +| **对象域 object** | 用户**已选择某个对象**(如某个项目/产品)后,对象内部的一切操作(任务、执行、工时、协办人、需求、成员维护等) | Service 上 `@CheckObjectPermission(objectType=..., objectId="#xxxId", permission="...")`,由 `ObjectPermissionAspect` → `ObjectPermissionService.checkPermission` 处理 | `system_role.scope_type='object'` + `system_menu.scope_type='object'` + `object_type` | + +红线: + +- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。 +- **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。 +- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。 +- 列表/详情这类对象内**读路径**目前未挂 `@CheckObjectPermission`(属已识别负债,台账 TD-001),新增读接口暂沿用现状即可,不要顺手改造,等独立立项。 + +判定口诀:**URL 里有 `{projectId}` / `{productId}` 等对象 ID → 对象域;没有 → 全域**。 + +## 接口语义(HTTP 动词) + +本仓库 update 类接口默认按 RESTful 标准用 HTTP 动词区分语义,前后端必须按下表对齐,避免"前端没传字段"和"前端想清空"在后端无法区分的歧义。 + +| 动词 | 语义 | 字段处理规则 | +|---|---|---| +| **PUT** | 全资源替换 | 前端必须把表单完整状态回传(读到的非必填字段也要原样回传)。后端按字段值落库:**有值=更新,`null`=清空**。DO 字段加 `@TableField(updateStrategy = FieldStrategy.ALWAYS)` 跳过全局 `NOT_NULL` 让 `null` 真的写库 | +| **PATCH** | 部分字段更新 | **本仓库暂不引入 PATCH 接口**。如果有"只改一两个字段"的需求,用专门的子动作接口(参考 `assignees/{id}/inactive`、`status` 这种语义化路径),不要在 update 接口里靠旁路标记(如 `clearXxx: true`)模拟 PATCH | +| **DELETE** | 资源删除 | 软删走全局 `deleted` 列,不需要参数 body | + +红线: + +- **不要在 update 类接口的 ReqVO 里加 `clearXxx: Boolean` 这种旁路标记**来模拟 PATCH —— 等于承认接口是"伪 PATCH",会让所有非必填字段都需要类似标记,长期污染 API 设计。需要部分更新就拆子动作接口。 +- 新增 update 接口时,必须在 API 文档对应章节明示"PUT 全字段回传"约定;DO 上对允许 null 的字段补 `FieldStrategy.ALWAYS` 注解,并加注释说明语义来源(指向本节)。 +- 历史接口若是稀疏 PATCH 风格(传 null = 不动),保留现状但不要拓展;遇到清空诉求时按 PUT 方向重构。 + ## 数据与 SQL - 新表 DO 复用现有 `BaseDO` / 审计字段风格,不要再引一套审计基类(除非该表本身明确不需要逻辑删除)。 diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageParam.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageParam.java index c5ee13a..756f252 100644 --- a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageParam.java +++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/pojo/PageParam.java @@ -27,9 +27,8 @@ public class PageParam implements Serializable { @Min(value = 1, message = "页码最小值为 1") private Integer pageNo = PAGE_NO; - @Schema(description = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @Schema(description = "每页条数,最大值为 200;传 -1 表示不分页(查询全部)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @NotNull(message = "每页条数不能为空") - @Min(value = 1, message = "每页条数最小值为 1") @Max(value = 200, message = "每页条数最大值为 200") private Integer pageSize = PAGE_SIZE; diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java index 2e0ceaa..b3b283b 100644 --- a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java +++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java @@ -33,6 +33,10 @@ public interface ErrorCodeConstants { 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, "产品状态定义不存在或已停用"); + // 产品创建原子接口(/create-with-team)专用:初始团队相关校验 + ErrorCode PRODUCT_INITIAL_TEAM_MANAGER_REQUIRED = new ErrorCode(1_008_001_024, "初始团队必须包含产品经理"); + ErrorCode PRODUCT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_001_025, "初始团队成员存在重复"); + ErrorCode PRODUCT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_001_026, "初始团队中存在非法角色"); // ========== 产品需求 1-008-002-000 ========== ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在"); @@ -84,15 +88,20 @@ public interface ErrorCodeConstants { ErrorCode PROJECT_MANAGER_TRANSFER_ROLE_INVALID = new ErrorCode(1_008_002_026, "原项目经理交接后的角色不能仍为项目经理"); ErrorCode PROJECT_MANAGER_MEMBER_NOT_ALLOW_DOWNGRADE = new ErrorCode(1_008_002_027, "当前项目经理不能直接调整为非经理角色,请先完成经理转交"); ErrorCode PROJECT_MAINLINE_DUPLICATE = new ErrorCode(1_008_002_028, "当前产品下已存在未作废的主线项目"); + // 项目创建原子接口(/create-with-team)专用:初始团队 + 方向一致性校验 + ErrorCode PROJECT_INITIAL_TEAM_MANAGER_REQUIRED = new ErrorCode(1_008_002_029, "初始团队必须包含项目经理"); + ErrorCode PROJECT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_002_030, "初始团队成员存在重复"); + ErrorCode PROJECT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_002_031, "初始团队中存在非法角色"); + ErrorCode PROJECT_DIRECTION_NOT_MATCH_PRODUCT = new ErrorCode(1_008_002_032, "项目方向与所属产品方向不一致"); // ========== 执行管理 1-008-003-000 ========== ErrorCode PROJECT_EXECUTION_NOT_EXISTS = new ErrorCode(1_008_003_000, "执行不存在"); ErrorCode PROJECT_EXECUTION_NAME_DUPLICATE = new ErrorCode(1_008_003_001, "当前项目下已经存在名称为【{}】的执行"); ErrorCode PROJECT_EXECUTION_OWNER_INVALID = new ErrorCode(1_008_003_002, "执行负责人必须是当前项目的有效成员"); - ErrorCode PROJECT_EXECUTION_MEMBER_INVALID = new ErrorCode(1_008_003_003, "执行成员必须是当前项目的有效成员"); - ErrorCode PROJECT_EXECUTION_MEMBER_ALREADY_EXISTS = new ErrorCode(1_008_003_004, "该用户已是当前执行的有效成员"); - ErrorCode PROJECT_EXECUTION_MEMBER_NOT_EXISTS = new ErrorCode(1_008_003_005, "执行成员不存在"); - ErrorCode PROJECT_EXECUTION_MEMBER_NOT_ACTIVE = new ErrorCode(1_008_003_006, "当前执行成员已失效"); + ErrorCode PROJECT_EXECUTION_ASSIGNEE_INVALID = new ErrorCode(1_008_003_003, "执行协办人必须是当前项目的有效成员"); + ErrorCode PROJECT_EXECUTION_ASSIGNEE_ALREADY_EXISTS = new ErrorCode(1_008_003_004, "该用户已是当前执行的有效协办人"); + ErrorCode PROJECT_EXECUTION_ASSIGNEE_NOT_EXISTS = new ErrorCode(1_008_003_005, "执行协办人不存在"); + ErrorCode PROJECT_EXECUTION_ASSIGNEE_NOT_ACTIVE = new ErrorCode(1_008_003_006, "当前执行协办人已失效"); ErrorCode PROJECT_EXECUTION_REQUIREMENT_NOT_READY = new ErrorCode(1_008_003_007, "当前阶段不支持给执行绑定项目需求"); ErrorCode PROJECT_EXECUTION_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_008, "当前项目状态不允许维护执行"); ErrorCode PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED = new ErrorCode(1_008_003_009, "该项目成员仍担任未终态执行负责人,请先完成执行负责人交接"); @@ -102,7 +111,12 @@ public interface ErrorCodeConstants { 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_MEMBER_REQUIRED = new ErrorCode(1_008_003_016, "创建执行时必须至少选择一名执行成员"); + 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_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, "确认执行名称与实际不一致"); + ErrorCode PROJECT_EXECUTION_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_003_021, "删除确认口令必须为 DELETE 或 删除"); // ========== 任务管理 1-008-004-000 ========== ErrorCode PROJECT_TASK_NOT_EXISTS = new ErrorCode(1_008_004_000, "任务不存在"); @@ -115,7 +129,10 @@ public interface ErrorCodeConstants { 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_PROGRESS_PARENT_NOT_EDITABLE = 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 或 删除"); ErrorCode PROJECT_TASK_LEAF_TO_PARENT_FORBIDDEN_PROGRESS = new ErrorCode(1_008_004_012, "拆子任务前请先将父任务进度清零"); ErrorCode PROJECT_TASK_LEAF_TO_PARENT_FORBIDDEN_WORKLOG = new ErrorCode(1_008_004_013, "拆子任务前请先删除父任务下已填的工时记录"); @@ -130,9 +147,19 @@ public interface ErrorCodeConstants { // ========== 任务工时 1_008_006_xxx ========== ErrorCode PROJECT_TASK_WORKLOG_NOT_EXISTS = new ErrorCode(1_008_006_001, "任务工时记录不存在"); ErrorCode PROJECT_TASK_WORKLOG_NOT_LEAF_TASK = new ErrorCode(1_008_006_002, "父任务不允许填报工时,请到具体子任务填报"); - ErrorCode PROJECT_TASK_WORKLOG_DURATION_INVALID = new ErrorCode(1_008_006_003, "工时时长必须大于 0 且为 30 分钟的整数倍"); + ErrorCode PROJECT_TASK_WORKLOG_DURATION_INVALID = new ErrorCode(1_008_006_003, "工时小时数必须大于 0 且为 0.5 的整数倍"); ErrorCode PROJECT_TASK_WORKLOG_NOT_OWNER_OR_ASSIGNEE = new ErrorCode(1_008_006_004, "仅任务负责人或在岗协办人可填报工时"); ErrorCode PROJECT_TASK_WORKLOG_EDIT_NOT_OWN = new ErrorCode(1_008_006_005, "只能修改自己填报的工时记录"); ErrorCode PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN = new ErrorCode(1_008_006_006, "仅记录填报人或任务负责人可删除该工时记录"); + ErrorCode PROJECT_TASK_WORKLOG_DATE_RANGE_INVALID = new ErrorCode(1_008_006_007, "段起始日期不能晚于段结束日期"); + ErrorCode PROJECT_TASK_WORKLOG_DATE_OVERLAP = new ErrorCode(1_008_006_008, "日期范围与该任务下您已有的工时记录重叠"); + ErrorCode PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC = new ErrorCode(1_008_006_010, "工时进度与日期顺序不一致:早段进度不得高于晚段、晚段进度不得低于早段"); + + // ========== 任务 / 工时附件 1_008_007_xxx ========== + ErrorCode PROJECT_TASK_ATTACHMENT_TOO_MANY = new ErrorCode(1_008_007_001, "附件数量不能超过 {} 个"); + ErrorCode PROJECT_TASK_ATTACHMENT_URL_INVALID = new ErrorCode(1_008_007_002, "附件地址非法,必须为 http/https URL 且长度不超过 1024"); + ErrorCode PROJECT_TASK_ATTACHMENT_NAME_INVALID = new ErrorCode(1_008_007_003, "附件文件名不合法(必填且长度不超过 255)"); + ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_NOT_ALLOWED = new ErrorCode(1_008_007_004, "附件扩展名【{}】不在允许列表内"); + ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_BLOCKED = new ErrorCode(1_008_007_005, "附件类型【{}】被禁止上传"); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ObjectActivityConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ObjectActivityConstants.java index 99c1b61..eea54f6 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ObjectActivityConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ObjectActivityConstants.java @@ -44,6 +44,22 @@ public final class ObjectActivityConstants { public static final String PROJECT_TRIGGER_CREATE_EXECUTION = "create_execution"; public static final String PROJECT_TRIGGER_CREATE_TASK = "create_task"; public static final String PROJECT_TRIGGER_SCHEDULE_REQUIREMENT = "schedule_requirement"; + /** + * 项目自动开始触发动作 —— 由执行 auto_start 反向触发。 + * 与 {@link #EXECUTION_TRIGGER_AUTO_START} 同语义;项目侧用此名走 AUTO_START_TRIGGERS 白名单。 + */ + public static final String PROJECT_TRIGGER_EXECUTION_AUTO_START = "execution_auto_start"; + + // ========== 任务自动推进触发动作 ========== + /** + * 任务自动开始动作编码 —— owner 或协办人填报工时时由后端自动触发的状态流转动作。 + * 对应 rdms_object_status_transition (object_type=task, action_code=auto_start, + * from=pending → to=active)。 + * 修改 DB 该条记录的 action_code 时必须同步修改本常量;本常量被 + * ProjectTaskServiceImpl#internalAutoStartByWorklog 引用。 + * 后续若再增加"任务自动完成 / 执行自动开始"等同类语义,按 *_ACTION_AUTO_* 命名加在本分组。 + */ + public static final String TASK_ACTION_AUTO_START = "auto_start"; // ========== 状态动作 ========== public static final String STATUS_ACTION_PAUSE = "pause"; @@ -57,21 +73,23 @@ public final class ObjectActivityConstants { public static final String MEMBER_ACTION_REMOVE = "remove_member"; public static final String EXECUTION_ACTION_CREATE = "create_execution_entity"; public static final String EXECUTION_ACTION_UPDATE = "update_execution_entity"; + public static final String EXECUTION_ACTION_DELETE = "delete_execution_entity"; public static final String EXECUTION_ACTION_CHANGE_OWNER = "change_execution_owner"; - public static final String EXECUTION_MEMBER_ACTION_ADD = "add_execution_member"; - public static final String EXECUTION_MEMBER_ACTION_REMOVE = "remove_execution_member"; + public static final String EXECUTION_ASSIGNEE_ACTION_ADD = "add_execution_assignee"; + public static final String EXECUTION_ASSIGNEE_ACTION_REMOVE = "remove_execution_assignee"; public static final String TASK_ACTION_CREATE = "create_task_entity"; public static final String TASK_ACTION_UPDATE = "update_task_entity"; + public static final String TASK_ACTION_DELETE = "delete_task_entity"; // ========== 任务协办人事件类型(B 模型 - 多行周期记录) ========== public static final String TASK_ASSIGNEE_ACTION_JOIN = "join"; public static final String TASK_ASSIGNEE_ACTION_INACTIVE = "inactive"; - // ========== 执行成员事件类型(B 模型 - 多行周期记录) ========== - public static final String EXECUTION_MEMBER_LOG_ACTION_JOIN = "join"; - public static final String EXECUTION_MEMBER_LOG_ACTION_INACTIVE = "inactive"; - public static final String EXECUTION_MEMBER_LOG_ACTION_OWNER_TRANSFER_IN = "owner_transfer_in"; - public static final String EXECUTION_MEMBER_LOG_ACTION_OWNER_TRANSFER_OUT = "owner_transfer_out"; + // ========== 执行协办人事件类型(B 模型 - 多行周期记录) ========== + public static final String EXECUTION_ASSIGNEE_LOG_ACTION_JOIN = "join"; + public static final String EXECUTION_ASSIGNEE_LOG_ACTION_INACTIVE = "inactive"; + public static final String EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN = "owner_transfer_in"; + public static final String EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_OUT = "owner_transfer_out"; public static final List STATUS_ACTION_TYPES = List.of( STATUS_ACTION_PAUSE, STATUS_ACTION_RESUME, STATUS_ACTION_ARCHIVE, STATUS_ACTION_ABANDON); @@ -97,21 +115,24 @@ public final class ObjectActivityConstants { case PRODUCT_ACTION_CREATE -> "创建"; case PRODUCT_ACTION_UPDATE -> "更新"; case PRODUCT_ACTION_DELETE -> "删除"; - case PROJECT_ACTION_AUTO_START -> "自动进入进行中"; + case PROJECT_ACTION_AUTO_START -> "开始推进"; case PROJECT_TRIGGER_CREATE_EXECUTION -> "创建执行"; case PROJECT_TRIGGER_CREATE_TASK -> "创建任务"; case PROJECT_TRIGGER_SCHEDULE_REQUIREMENT -> "项目需求排期"; + case PROJECT_TRIGGER_EXECUTION_AUTO_START -> "执行自动开始"; case EXECUTION_ACTION_CREATE -> "创建执行"; case EXECUTION_ACTION_UPDATE -> "更新执行"; + case EXECUTION_ACTION_DELETE -> "删除执行"; case EXECUTION_ACTION_CHANGE_OWNER -> "变更执行负责人"; - case EXECUTION_MEMBER_ACTION_ADD -> "新增执行成员"; - case EXECUTION_MEMBER_ACTION_REMOVE -> "移出执行成员"; + case EXECUTION_ASSIGNEE_ACTION_ADD -> "新增执行协办人"; + case EXECUTION_ASSIGNEE_ACTION_REMOVE -> "移出执行协办人"; case TASK_ACTION_CREATE -> "创建任务"; case TASK_ACTION_UPDATE -> "更新任务"; + case TASK_ACTION_DELETE -> "删除任务"; case TASK_ASSIGNEE_ACTION_JOIN -> "加入"; case TASK_ASSIGNEE_ACTION_INACTIVE -> "退出"; - case EXECUTION_MEMBER_LOG_ACTION_OWNER_TRANSFER_IN -> "转入负责人"; - case EXECUTION_MEMBER_LOG_ACTION_OWNER_TRANSFER_OUT -> "转出负责人"; + case EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN -> "转入负责人"; + case EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_OUT -> "转出负责人"; case "start" -> "开始"; case "block" -> "阻塞"; case "complete" -> "完成"; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectExecutionConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectExecutionConstants.java index bf74a22..920685e 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectExecutionConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectExecutionConstants.java @@ -1,5 +1,7 @@ package com.njcn.rdms.module.project.constant; +import java.util.Set; + /** * 执行对象常量。 */ @@ -34,13 +36,25 @@ public final class ProjectExecutionConstants { public static final String PERMISSION_OWNER = "project:execution:owner"; /** - * 执行成员治理权限码。 + * 执行协办人治理权限码。 */ - public static final String PERMISSION_MEMBER = "project:execution:member"; + public static final String PERMISSION_ASSIGNEE = "project:execution:assignee"; /** * 执行状态动作权限码。 */ public static final String PERMISSION_STATUS = "project:execution:status"; + /** + * 删除执行权限码。 + * 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。 + */ + public static final String PERMISSION_DELETE = "project:execution:delete"; + + /** + * 删除确认口令合法值集合;兼容大写英文 "DELETE" 与中文 "删除",前端可纯中文文案。 + * 校验时精确匹配(trim 后比对)。 + */ + public static final Set DELETE_CONFIRM_TEXTS = Set.of("DELETE", "删除"); + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java index 096ea8e..84f068f 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectObjectConstants.java @@ -71,6 +71,7 @@ public final class ProjectObjectConstants { public static final Set AUTO_START_TRIGGERS = Set.of( ObjectActivityConstants.PROJECT_TRIGGER_CREATE_EXECUTION, ObjectActivityConstants.PROJECT_TRIGGER_CREATE_TASK, - ObjectActivityConstants.PROJECT_TRIGGER_SCHEDULE_REQUIREMENT); + ObjectActivityConstants.PROJECT_TRIGGER_SCHEDULE_REQUIREMENT, + ObjectActivityConstants.PROJECT_TRIGGER_EXECUTION_AUTO_START); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectTaskConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectTaskConstants.java index 9f60dca..3aa7b6f 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectTaskConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ProjectTaskConstants.java @@ -1,5 +1,7 @@ package com.njcn.rdms.module.project.constant; +import java.util.Set; + /** * 任务对象常量。 */ @@ -43,4 +45,18 @@ public final class ProjectTaskConstants { */ public static final String PERMISSION_WORKLOG = "project:task:worklog"; + /** + * 删除任务权限码。 + * 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。 + * 实际拦截:service 内按 task.parentTaskId 分流走 checkOwnerOrProjectPermission + * (一级任务 → execution.ownerId 字段身份;子任务 → parentTask.ownerId 字段身份;不命中字段时回落本权限码)。 + */ + public static final String PERMISSION_DELETE = "project:task:delete"; + + /** + * 删除确认口令合法值集合;兼容大写英文 "DELETE" 与中文 "删除",前端可纯中文文案。 + * 校验时精确匹配(trim 后比对)。 + */ + public static final Set DELETE_CONFIRM_TEXTS = Set.of("DELETE", "删除"); + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java index a6b8e10..bf62cc3 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java @@ -4,6 +4,7 @@ import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.common.util.object.BeanUtils; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductCreateWithTeamReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO; @@ -39,6 +40,13 @@ public class ProductController { return success(productService.createProduct(createReqVO)); } + @PostMapping("/create-with-team") + @Operation(summary = "创建产品并初始化团队(原子接口)") + @PreAuthorize("@ss.hasPermission('project:product:create')") + public CommonResult createProductWithTeam(@Valid @RequestBody ProductCreateWithTeamReqVO reqVO) { + return success(productService.createProductWithTeam(reqVO)); + } + @PutMapping("/update") @Operation(summary = "更新产品") public CommonResult updateProduct(@Valid @RequestBody ProductSaveReqVO updateReqVO) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductCreateWithTeamReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductCreateWithTeamReqVO.java new file mode 100644 index 0000000..d4d9b84 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductCreateWithTeamReqVO.java @@ -0,0 +1,42 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.product; + +import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * 产品创建原子接口请求 VO(POST /project/product/create-with-team)。 + * + *

由前端"产品两步向导"一次性提交:产品基础资料 + 初始团队成员。 + * 后端必须在同一事务内完成全部写入,任一步失败整体回滚。 + */ +@Schema(description = "管理后台 - 产品创建(含初始团队)Request VO") +@Data +public class ProductCreateWithTeamReqVO { + + /** + * 产品基础资料。沿用 {@link ProductSaveReqVO} 字段约束(同 POST /create)。 + */ + @Schema(description = "产品基础资料", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "产品基础资料不能为空") + @Valid + private ProductSaveReqVO product; + + /** + * 初始团队成员列表。 + * + *

必须包含一条 {@code userId == product.managerUserId} 的产品经理成员, + * 由前端在打开第 2 步时按 role code = {@code product_manager} 反查 roleId 后聚合提交。 + * 后端不再根据 {@code product.managerUserId} 自动追加经理成员。 + */ + @Schema(description = "初始团队成员(含产品经理本人)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "初始团队成员不能为空") + @Valid + private List members; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectController.java index 92d36b2..6f3ca18 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectController.java @@ -4,6 +4,7 @@ import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.common.util.object.BeanUtils; import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectContextRespVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectCreateWithTeamReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectDeleteReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectOverviewSummaryRespVO; import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectPageReqVO; @@ -41,6 +42,13 @@ public class ProjectController { return success(projectService.createProject(createReqVO)); } + @PostMapping("/create-with-team") + @Operation(summary = "创建项目并初始化团队(原子接口)") + @PreAuthorize("@ss.hasPermission('project:project:create')") + public CommonResult createProjectWithTeam(@Valid @RequestBody ProjectCreateWithTeamReqVO reqVO) { + return success(projectService.createProjectWithTeam(reqVO)); + } + @PutMapping("/update") @Operation(summary = "更新项目") public CommonResult updateProject(@Valid @RequestBody ProjectSaveReqVO updateReqVO) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionAssigneeController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionAssigneeController.java new file mode 100644 index 0000000..0f913cc --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionAssigneeController.java @@ -0,0 +1,70 @@ +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.PageResult; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeInactiveReqVO; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogPageReqVO; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogRespVO; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeRespVO; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeSaveReqVO; +import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionAssigneeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 执行协办人") +@RestController +@RequestMapping("/project/project/{projectId}/executions/{executionId}") +@Validated +public class ProjectExecutionAssigneeController { + + @Resource + private ProjectExecutionAssigneeService projectExecutionAssigneeService; + + @GetMapping("/assignees") + @Operation(summary = "获取执行协办人列表(仅当前活跃)") + public CommonResult> getExecutionAssigneeList(@PathVariable("projectId") Long projectId, + @PathVariable("executionId") Long executionId) { + return success(projectExecutionAssigneeService.getExecutionAssigneeList(projectId, executionId)); + } + + @PostMapping("/assignees") + @Operation(summary = "新增执行协办人(B 模型 - 每次新增一段)") + public CommonResult createExecutionAssignee(@PathVariable("projectId") Long projectId, + @PathVariable("executionId") Long executionId, + @Valid @RequestBody ExecutionAssigneeSaveReqVO reqVO) { + return success(projectExecutionAssigneeService.createExecutionAssignee(projectId, executionId, reqVO)); + } + + @PostMapping("/assignees/{assigneeId}/inactive") + @Operation(summary = "失效执行协办人(永久保留 removedReason)") + public CommonResult inactiveExecutionAssignee(@PathVariable("projectId") Long projectId, + @PathVariable("executionId") Long executionId, + @PathVariable("assigneeId") Long assigneeId, + @Valid @RequestBody ExecutionAssigneeInactiveReqVO reqVO) { + projectExecutionAssigneeService.inactiveExecutionAssignee(projectId, executionId, assigneeId, reqVO); + return success(true); + } + + @GetMapping("/assignee-logs") + @Operation(summary = "获取执行协办人变更历史(分页)") + public CommonResult> getExecutionAssigneeLogPage( + @PathVariable("projectId") Long projectId, + @PathVariable("executionId") Long executionId, + @Valid ExecutionAssigneeLogPageReqVO reqVO) { + return success(projectExecutionAssigneeService.getExecutionAssigneeLogPage(projectId, executionId, reqVO)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionController.java index 09331ce..0e80c45 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionController.java @@ -7,7 +7,9 @@ import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execut import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardRespVO; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO; -import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionDeleteReqVO; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionUpdateReqVO; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusActionReqVO; import com.njcn.rdms.module.project.service.project.ProjectStatusBoardService; import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService; @@ -34,7 +36,7 @@ public class ProjectExecutionController { @PostMapping @Operation(summary = "创建执行") public CommonResult createExecution(@PathVariable("projectId") Long projectId, - @Valid @RequestBody ProjectExecutionSaveReqVO reqVO) { + @Valid @RequestBody ProjectExecutionCreateReqVO reqVO) { return success(projectExecutionService.createExecution(projectId, reqVO)); } @@ -42,7 +44,7 @@ public class ProjectExecutionController { @Operation(summary = "编辑执行") public CommonResult updateExecution(@PathVariable("projectId") Long projectId, @PathVariable("executionId") Long executionId, - @Valid @RequestBody ProjectExecutionSaveReqVO reqVO) { + @Valid @RequestBody ProjectExecutionUpdateReqVO reqVO) { reqVO.setId(executionId); projectExecutionService.updateExecution(projectId, reqVO); return success(true); @@ -87,4 +89,13 @@ public class ProjectExecutionController { return success(true); } + @DeleteMapping("/{executionId}") + @Operation(summary = "删除执行(仅初始态可删,三重确认)") + public CommonResult deleteExecution(@PathVariable("projectId") Long projectId, + @PathVariable("executionId") Long executionId, + @Valid @RequestBody ProjectExecutionDeleteReqVO reqVO) { + projectExecutionService.deleteExecution(projectId, executionId, reqVO); + return success(true); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionMemberController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionMemberController.java deleted file mode 100644 index 2931ba4..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/ProjectExecutionMemberController.java +++ /dev/null @@ -1,70 +0,0 @@ -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.PageResult; -import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberInactiveReqVO; -import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberLogPageReqVO; -import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberLogRespVO; -import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberRespVO; -import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberSaveReqVO; -import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionMemberService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -import static com.njcn.rdms.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - 执行成员") -@RestController -@RequestMapping("/project/project/{projectId}/executions/{executionId}") -@Validated -public class ProjectExecutionMemberController { - - @Resource - private ProjectExecutionMemberService projectExecutionMemberService; - - @GetMapping("/members") - @Operation(summary = "获取执行成员列表(仅当前活跃)") - public CommonResult> getExecutionMemberList(@PathVariable("projectId") Long projectId, - @PathVariable("executionId") Long executionId) { - return success(projectExecutionMemberService.getExecutionMemberList(projectId, executionId)); - } - - @PostMapping("/members") - @Operation(summary = "新增执行成员(B 模型 - 每次新增一段)") - public CommonResult createExecutionMember(@PathVariable("projectId") Long projectId, - @PathVariable("executionId") Long executionId, - @Valid @RequestBody ExecutionMemberSaveReqVO reqVO) { - return success(projectExecutionMemberService.createExecutionMember(projectId, executionId, reqVO)); - } - - @PostMapping("/members/{memberId}/inactive") - @Operation(summary = "失效执行成员(永久保留 removedReason)") - public CommonResult inactiveExecutionMember(@PathVariable("projectId") Long projectId, - @PathVariable("executionId") Long executionId, - @PathVariable("memberId") Long memberId, - @Valid @RequestBody ExecutionMemberInactiveReqVO reqVO) { - projectExecutionMemberService.inactiveExecutionMember(projectId, executionId, memberId, reqVO); - return success(true); - } - - @GetMapping("/member-logs") - @Operation(summary = "获取执行成员变更历史(分页)") - public CommonResult> getExecutionMemberLogPage( - @PathVariable("projectId") Long projectId, - @PathVariable("executionId") Long executionId, - @Valid ExecutionMemberLogPageReqVO reqVO) { - return success(projectExecutionMemberService.getExecutionMemberLogPage(projectId, executionId, reqVO)); - } - -} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberInactiveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeInactiveReqVO.java similarity index 78% rename from rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberInactiveReqVO.java rename to rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeInactiveReqVO.java index 63f2642..30e9acc 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberInactiveReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeInactiveReqVO.java @@ -1,13 +1,13 @@ -package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member; +package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; -@Schema(description = "管理后台 - 执行成员失效 Request VO") +@Schema(description = "管理后台 - 执行协办人失效 Request VO") @Data -public class ExecutionMemberInactiveReqVO { +public class ExecutionAssigneeInactiveReqVO { @Schema(description = "失效原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "阶段性退出") @NotBlank(message = "失效原因不能为空") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberLogPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeLogPageReqVO.java similarity index 76% rename from rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberLogPageReqVO.java rename to rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeLogPageReqVO.java index 29c40ce..d71bc22 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberLogPageReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeLogPageReqVO.java @@ -1,4 +1,4 @@ -package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member; +package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee; import com.njcn.rdms.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; @@ -8,16 +8,16 @@ import lombok.EqualsAndHashCode; import java.time.LocalDateTime; import java.util.List; -@Schema(description = "管理后台 - 执行成员变更历史分页 Request VO") +@Schema(description = "管理后台 - 执行协办人变更历史分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) -public class ExecutionMemberLogPageReqVO extends PageParam { +public class ExecutionAssigneeLogPageReqVO extends PageParam { @Schema(description = "事件类型多选;不传表示全部", example = "[\"join\",\"inactive\",\"owner_transfer_in\",\"owner_transfer_out\"]") private List actionTypes; - @Schema(description = "成员用户编号;不传表示全部") + @Schema(description = "协办人用户编号;不传表示全部") private Long userId; @Schema(description = "起始时间(含),按 actionTime 比较") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberLogRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeLogRespVO.java similarity index 88% rename from rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberLogRespVO.java rename to rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeLogRespVO.java index 716264a..9e2c2fa 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberLogRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeLogRespVO.java @@ -1,13 +1,13 @@ -package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member; +package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; -@Schema(description = "管理后台 - 执行成员变更历史 Response VO") +@Schema(description = "管理后台 - 执行协办人变更历史 Response VO") @Data -public class ExecutionMemberLogRespVO { +public class ExecutionAssigneeLogRespVO { @Schema(description = "日志编号", example = "12001") private Long id; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeRespVO.java similarity index 62% rename from rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberRespVO.java rename to rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeRespVO.java index 3149def..6d44dae 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeRespVO.java @@ -1,21 +1,21 @@ -package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member; +package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; -@Schema(description = "管理后台 - 执行成员 Response VO") +@Schema(description = "管理后台 - 执行协办人 Response VO") @Data -public class ExecutionMemberRespVO { +public class ExecutionAssigneeRespVO { - @Schema(description = "成员关系编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7001") + @Schema(description = "协办人关系编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7001") private Long id; @Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001") private Long executionId; - @Schema(description = "成员用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002") + @Schema(description = "协办人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002") private Long userId; - @Schema(description = "成员用户昵称") + @Schema(description = "协办人用户昵称") private String userNickname; @Schema(description = "加入时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime joinedAt; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeSaveReqVO.java new file mode 100644 index 0000000..b575ea9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/assignee/ExecutionAssigneeSaveReqVO.java @@ -0,0 +1,15 @@ +package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 执行协办人新增 Request VO") +@Data +public class ExecutionAssigneeSaveReqVO { + + @Schema(description = "协办人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002") + @NotNull(message = "协办人用户不能为空") + private Long userId; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionCreateReqVO.java similarity index 77% rename from rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionSaveReqVO.java rename to rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionCreateReqVO.java index 8f723e6..464f6fb 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionSaveReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionCreateReqVO.java @@ -9,12 +9,15 @@ import lombok.Data; import java.time.LocalDate; import java.util.List; -@Schema(description = "管理后台 - 执行保存 Request VO") +/** + * 创建执行 Request VO。 + *

+ * 含 ownerId(必填)+ assigneeUserIds(创建时同步装配协办人)。 + * 后续编辑主数据走 PUT + {@link ProjectExecutionUpdateReqVO}(不含 ownerId / 协办人字段,避免越权裂缝)。 + */ +@Schema(description = "管理后台 - 执行创建 Request VO") @Data -public class ProjectExecutionSaveReqVO { - - @Schema(description = "执行编号", example = "5001") - private Long id; +public class ProjectExecutionCreateReqVO { @Schema(description = "执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调") @NotBlank(message = "执行名称不能为空") @@ -44,7 +47,7 @@ public class ProjectExecutionSaveReqVO { @Size(max = 200000, message = "执行说明长度不能超过200000个字符") private String executionDesc; - @Schema(description = "创建执行时同步写入的成员用户编号列表;编辑执行主数据时不维护成员", example = "[3002,3003]") - private List memberUserIds; + @Schema(description = "创建执行时同步写入的协办人用户编号列表", example = "[3002,3003]") + private List assigneeUserIds; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionDeleteReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionDeleteReqVO.java new file mode 100644 index 0000000..f159dc1 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionDeleteReqVO.java @@ -0,0 +1,31 @@ +package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 删除执行 Request VO。与项目侧 ProjectDeleteReqVO 同款三重确认(confirmText / executionName / reason)。 + * 详见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.2.1。 + */ +@Schema(description = "管理后台 - 执行删除 Request VO") +@Data +public class ProjectExecutionDeleteReqVO { + + @Schema(description = "确认输入的执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调") + @NotBlank(message = "确认执行名称不能为空") + @Size(max = 200, message = "确认执行名称长度不能超过200个字符") + private String executionName; + + @Schema(description = "删除确认口令,当前固定输入 DELETE", requiredMode = Schema.RequiredMode.REQUIRED, example = "DELETE") + @NotBlank(message = "删除确认口令不能为空") + @Size(max = 32, message = "删除确认口令长度不能超过32个字符") + private String confirmText; + + @Schema(description = "删除原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "执行录入错误") + @NotBlank(message = "删除原因不能为空") + @Size(max = 500, message = "删除原因长度不能超过500个字符") + private String reason; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionRespVO.java index 93a3339..d187f16 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionRespVO.java @@ -44,7 +44,7 @@ public class ProjectExecutionRespVO { private LocalDate actualStartDate; @Schema(description = "实际结束日期") private LocalDate actualEndDate; - @Schema(description = "执行进度缓存值") + @Schema(description = "执行进度") private BigDecimal progressRate; @Schema(description = "执行说明") private String executionDesc; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionUpdateReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionUpdateReqVO.java new file mode 100644 index 0000000..4d391ab --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/execution/ProjectExecutionUpdateReqVO.java @@ -0,0 +1,47 @@ +package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDate; + +/** + * 编辑执行 Request VO(PUT 全字段语义)。 + *

+ * 不含 ownerId / assigneeUserIds:换负责人走 /change-owner 独立端点,协办人维护走 /assignees 独立端点。 + * 详见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.3。 + */ +@Schema(description = "管理后台 - 执行编辑 Request VO") +@Data +public class ProjectExecutionUpdateReqVO { + + @Schema(description = "执行编号", example = "5001") + private Long id; + + @Schema(description = "执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调") + @NotBlank(message = "执行名称不能为空") + @Size(max = 200, message = "执行名称长度不能超过200个字符") + private String executionName; + + @Schema(description = "执行类型,取值来自字典 rdms_project_execution_type", requiredMode = Schema.RequiredMode.REQUIRED, example = "feature") + @NotBlank(message = "执行类型不能为空") + @Size(max = 32, message = "执行类型长度不能超过32个字符") + private String executionType; + + @Schema(description = "关联项目需求编号,第一阶段只接受空值", example = "9001") + private Long projectRequirementId; + + @Schema(description = "计划开始日期") + private LocalDate plannedStartDate; + + @Schema(description = "计划结束日期") + private LocalDate plannedEndDate; + + @Schema(description = "执行说明(接受 HTML 富文本,图片走 URL 引用;后端经全局 XSS Safelist 自动净化)", + example = "接口联调与问题跟踪") + @Size(max = 200000, message = "执行说明长度不能超过200000个字符") + private String executionDesc; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberSaveReqVO.java deleted file mode 100644 index 5324cb7..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/execution/vo/member/ExecutionMemberSaveReqVO.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Schema(description = "管理后台 - 执行成员新增 Request VO") -@Data -public class ExecutionMemberSaveReqVO { - - @Schema(description = "成员用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002") - @NotNull(message = "成员用户不能为空") - private Long userId; - -} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/ProjectTaskController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/ProjectTaskController.java index f5c246e..5e0ffe4 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/ProjectTaskController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/ProjectTaskController.java @@ -2,6 +2,7 @@ package com.njcn.rdms.module.project.controller.admin.project.task; import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskDeleteReqVO; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO; @@ -83,4 +84,14 @@ public class ProjectTaskController { return success(true); } + @DeleteMapping("/{taskId}") + @Operation(summary = "删除任务(仅初始态可删,三重确认 + 执行 owner 硬卡)") + public CommonResult deleteTask(@PathVariable("projectId") Long projectId, + @PathVariable("executionId") Long executionId, + @PathVariable("taskId") Long taskId, + @Valid @RequestBody ProjectTaskDeleteReqVO reqVO) { + projectTaskService.deleteTask(projectId, executionId, taskId, reqVO); + return success(true); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/TaskAssigneeController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/TaskAssigneeController.java index 584e71c..cca3c7f 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/TaskAssigneeController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/TaskAssigneeController.java @@ -12,7 +12,6 @@ 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.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -44,7 +43,6 @@ public class TaskAssigneeController { @PostMapping("/assignees") @Operation(summary = "加入任务协办人") - @PreAuthorize("@ss.hasPermission('project:task:assignee')") public CommonResult createAssignee(@PathVariable("projectId") Long projectId, @PathVariable("executionId") Long executionId, @PathVariable("taskId") Long taskId, @@ -54,7 +52,6 @@ public class TaskAssigneeController { @PostMapping("/assignees/{assigneeId}/inactive") @Operation(summary = "退出任务协办人") - @PreAuthorize("@ss.hasPermission('project:task:assignee')") public CommonResult inactiveAssignee(@PathVariable("projectId") Long projectId, @PathVariable("executionId") Long executionId, @PathVariable("taskId") Long taskId, diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/TaskWorklogController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/TaskWorklogController.java index 83f06e5..7d4b7f9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/TaskWorklogController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/TaskWorklogController.java @@ -10,7 +10,6 @@ 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.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -43,7 +42,6 @@ public class TaskWorklogController { @PostMapping("/worklogs") @Operation(summary = "新增任务工时") - @PreAuthorize("@ss.hasPermission('project:task:worklog')") public CommonResult createWorklog(@PathVariable("projectId") Long projectId, @PathVariable("executionId") Long executionId, @PathVariable("taskId") Long taskId, @@ -53,7 +51,6 @@ public class TaskWorklogController { @PutMapping("/worklogs/{worklogId}") @Operation(summary = "修改任务工时(仅自己)") - @PreAuthorize("@ss.hasPermission('project:task:worklog')") public CommonResult updateWorklog(@PathVariable("projectId") Long projectId, @PathVariable("executionId") Long executionId, @PathVariable("taskId") Long taskId, @@ -65,7 +62,6 @@ public class TaskWorklogController { @DeleteMapping("/worklogs/{worklogId}") @Operation(summary = "删除任务工时(自己或任务负责人)") - @PreAuthorize("@ss.hasPermission('project:task:worklog')") public CommonResult deleteWorklog(@PathVariable("projectId") Long projectId, @PathVariable("executionId") Long executionId, @PathVariable("taskId") Long taskId, diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskDeleteReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskDeleteReqVO.java new file mode 100644 index 0000000..5266407 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskDeleteReqVO.java @@ -0,0 +1,31 @@ +package com.njcn.rdms.module.project.controller.admin.project.task.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 删除任务 Request VO。与执行侧 / 项目侧同款三重确认(confirmText / taskName / reason)。 + * 详见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.2.2。 + */ +@Schema(description = "管理后台 - 任务删除 Request VO") +@Data +public class ProjectTaskDeleteReqVO { + + @Schema(description = "确认输入的任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "接口契约对齐") + @NotBlank(message = "确认任务名称不能为空") + @Size(max = 200, message = "确认任务名称长度不能超过200个字符") + private String taskName; + + @Schema(description = "删除确认口令,当前固定输入 DELETE", requiredMode = Schema.RequiredMode.REQUIRED, example = "DELETE") + @NotBlank(message = "删除确认口令不能为空") + @Size(max = 32, message = "删除确认口令长度不能超过32个字符") + private String confirmText; + + @Schema(description = "删除原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "任务录入错误") + @NotBlank(message = "删除原因不能为空") + @Size(max = 500, message = "删除原因长度不能超过500个字符") + private String reason; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskRespVO.java index 661b972..10bb362 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskRespVO.java @@ -1,5 +1,6 @@ package com.njcn.rdms.module.project.controller.admin.project.task.vo; +import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -8,6 +9,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; + + @Schema(description = "管理后台 - 任务 Response VO") @Data public class ProjectTaskRespVO { @@ -20,6 +23,12 @@ public class ProjectTaskRespVO { private Long executionId; @Schema(description = "父任务编号") private Long parentTaskId; + @Schema(description = "父任务负责人用户编号;一级任务为 null,子任务用于前端判断" + + "新增子任务/编辑/删除按钮显隐(详见前端联调清单 §4.3)", example = "3002") + private Long parentTaskOwnerId; + @Schema(description = "所属执行的负责人用户编号;前端判断一级任务" + + "新增/编辑/删除按钮显隐(一级任务执行负责人可自行调整)", example = "3001") + private Long executionOwnerId; @Schema(description = "任务标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "接口联调任务") private String taskTitle; @Schema(description = "任务负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002") @@ -52,9 +61,11 @@ public class ProjectTaskRespVO { private String lastStatusReason; @Schema(description = "当前活跃协办人列表;详细变更历史见 assignee-logs 接口") private List assignees; - @Schema(description = "已填报工时合计(分钟);逻辑删除的工时记录不计入。无记录默认为 0", - example = "300") - private Long totalSpentMinutes; + @Schema(description = "已填报工时合计(小时,0.5 颗粒);逻辑删除的工时记录不计入。无记录默认为 0", + example = "8.0") + private BigDecimal totalSpentHours; + @Schema(description = "附件列表") + private List attachments; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskSaveReqVO.java index bd3df2a..9579dd9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskSaveReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskSaveReqVO.java @@ -1,13 +1,12 @@ package com.njcn.rdms.module.project.controller.admin.project.task.vo; +import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.DecimalMax; -import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; -import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; @@ -29,11 +28,6 @@ public class ProjectTaskSaveReqVO { @Schema(description = "任务负责人用户编号;子任务不传时继承父任务负责人", example = "3002") private Long ownerId; - @Schema(description = "任务进度", example = "0.00") - @DecimalMin(value = "0.00", message = "任务进度不能小于0") - @DecimalMax(value = "100.00", message = "任务进度不能大于100") - private BigDecimal progressRate; - @Schema(description = "计划开始日期") private LocalDate plannedStartDate; @@ -49,5 +43,9 @@ public class ProjectTaskSaveReqVO { + "协办人通过独立接口管理,详见 /tasks/{id}/assignees") private List assigneeUserIds; + @Schema(description = "附件列表;规则与限制详见 AttachmentValidator(数量上限、扩展名白/黑名单、URL 协议)") + @Valid + private List attachments; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskStatusActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskStatusActionReqVO.java index c65baed..b7e4e00 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskStatusActionReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/ProjectTaskStatusActionReqVO.java @@ -9,7 +9,7 @@ import lombok.Data; @Data public class ProjectTaskStatusActionReqVO { - @Schema(description = "动作编码,如 start、block、resume、complete、cancel", requiredMode = Schema.RequiredMode.REQUIRED, example = "complete") + @Schema(description = "动作编码,如 auto_start、pause、resume、complete、cancel", requiredMode = Schema.RequiredMode.REQUIRED, example = "complete") @NotBlank(message = "动作编码不能为空") @Size(max = 32, message = "动作编码长度不能超过32个字符") private String actionCode; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogPageReqVO.java index 550165e..4b86a4e 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogPageReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogPageReqVO.java @@ -15,10 +15,10 @@ public class TaskWorklogPageReqVO extends PageParam { @Schema(description = "填报人用户编号;不传表示全部") private Long userId; - @Schema(description = "起始日期(含),按 workDate 比较") + @Schema(description = "查询区间起始日期(含),按段相交过滤(record.endDate >= startDate)") private LocalDate startDate; - @Schema(description = "截止日期(含),按 workDate 比较") + @Schema(description = "查询区间截止日期(含),按段相交过滤(record.startDate <= endDate)") private LocalDate endDate; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogRespVO.java index 5898306..1854cec 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogRespVO.java @@ -1,10 +1,13 @@ package com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog; +import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - 任务工时 Response VO") @Data @@ -22,15 +25,24 @@ public class TaskWorklogRespVO { @Schema(description = "填报人昵称", example = "张三") private String userNickname; - @Schema(description = "工作日期", example = "2026-05-08") - private LocalDate workDate; + @Schema(description = "段起始日期(含),单天=与 endDate 相等", example = "2026-05-04") + private LocalDate startDate; - @Schema(description = "时长(分钟)", example = "150") - private Integer durationMinutes; + @Schema(description = "段结束日期(含),单天=与 startDate 相等", example = "2026-05-08") + private LocalDate endDate; + + @Schema(description = "本次填报小时数(0.5 颗粒)", example = "8.0") + private BigDecimal durationHours; + + @Schema(description = "本次填报进度(0~100)", example = "60.00") + private BigDecimal progressRate; @Schema(description = "工作内容描述") private String workContent; + @Schema(description = "附件列表") + private List attachments; + @Schema(description = "创建时间") private LocalDateTime createTime; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogSaveReqVO.java index bb5b272..6a851c8 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogSaveReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/task/vo/worklog/TaskWorklogSaveReqVO.java @@ -1,33 +1,57 @@ package com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog; +import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Min; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; +import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; /** * 任务工时新增/更新请求。同表共用:updateWorklog 不接受 taskId / userId 切换,前端无需也无法传。 - * 时长颗粒(30 分钟整数倍)由 Service 层校验。 + * 段语义:startDate / endDate 必填,单天 = 二者相等;同人同任务下日期范围禁止重叠(Service 层校验)。 + * 颗粒:durationHours 必须 > 0 且为 0.5 的整数倍(Service 层校验)。 */ @Schema(description = "管理后台 - 任务工时 Save Request VO") @Data public class TaskWorklogSaveReqVO { - @Schema(description = "工作日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-08") - @NotNull(message = "工作日期不能为空") - private LocalDate workDate; + @Schema(description = "段起始日期(含),单天=与 endDate 相等", + requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-04") + @NotNull(message = "段起始日期不能为空") + private LocalDate startDate; - @Schema(description = "时长(分钟),> 0 且必须为 30 的整数倍", - requiredMode = Schema.RequiredMode.REQUIRED, example = "150") - @NotNull(message = "工时时长不能为空") - @Min(value = 30, message = "工时时长必须大于 0 且为 30 分钟的整数倍") - private Integer durationMinutes; + @Schema(description = "段结束日期(含),单天=与 startDate 相等", + requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-08") + @NotNull(message = "段结束日期不能为空") + private LocalDate endDate; + + @Schema(description = "本次填报小时数,> 0 且必须为 0.5 的整数倍", + requiredMode = Schema.RequiredMode.REQUIRED, example = "8.0") + @NotNull(message = "工时小时数不能为空") + @DecimalMin(value = "0.5", message = "工时小时数必须大于 0 且为 0.5 的整数倍") + private BigDecimal durationHours; + + @Schema(description = "本次填报进度(0~100,必填)。owner 填报:以 owner 本人最新一条工时(按 end_date 排序)" + + "为准同步任务进度;协作人填报:仅作为本人自评个人完成度,不影响任务/父任务进度", + requiredMode = Schema.RequiredMode.REQUIRED, example = "60.00") + @NotNull(message = "本次填报进度不能为空") + @DecimalMin(value = "0.00", message = "本次填报进度不能小于 0") + @DecimalMax(value = "100.00", message = "本次填报进度不能大于 100") + private BigDecimal progressRate; @Schema(description = "工作内容描述", example = "完成接口联调与冒烟测试") @Size(max = 2000, message = "工作内容长度不能超过 2000 个字符") private String workContent; + @Schema(description = "附件列表;规则与限制详见 AttachmentValidator(数量上限、扩展名白/黑名单、URL 协议)") + @Valid + private List attachments; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/project/ProjectCreateWithTeamReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/project/ProjectCreateWithTeamReqVO.java new file mode 100644 index 0000000..ea77267 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/project/ProjectCreateWithTeamReqVO.java @@ -0,0 +1,43 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.project; + +import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * 项目创建原子接口请求 VO(POST /project/project/create-with-team)。 + * + *

由前端"项目两步向导"一次性提交:项目基础资料 + 初始团队成员。 + * 后端必须在同一事务内完成全部写入,任一步失败整体回滚。 + */ +@Schema(description = "管理后台 - 项目创建(含初始团队)Request VO") +@Data +public class ProjectCreateWithTeamReqVO { + + /** + * 项目基础资料。沿用 {@link ProjectSaveReqVO} 字段约束(同 POST /create)。 + * 新增场景前端不传 {@code actualStartDate / actualEndDate}(实际日期由项目执行阶段才有值)。 + */ + @Schema(description = "项目基础资料", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "项目基础资料不能为空") + @Valid + private ProjectSaveReqVO project; + + /** + * 初始团队成员列表。 + * + *

必须包含一条 {@code userId == project.managerUserId} 的项目经理成员, + * 由前端在打开第 2 步时按 role code = {@code project_manager} 反查 roleId 后聚合提交。 + * 后端不再根据 {@code project.managerUserId} 自动追加经理成员。 + */ + @Schema(description = "初始团队成员(含项目经理本人)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "初始团队成员不能为空") + @Valid + private List members; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/attachment/AttachmentItem.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/attachment/AttachmentItem.java new file mode 100644 index 0000000..df1e68e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/attachment/AttachmentItem.java @@ -0,0 +1,37 @@ +package com.njcn.rdms.module.project.dal.dataobject.attachment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 附件项。 + *

+ * 既作为业务表 {@code attachments} JSON 列的元素类型,也直接用于 ReqVO/RespVO 透传。 + * 文件本身由 rdms-system 的 file 模块上传到对象存储,本对象仅记录元数据。 + */ +@Schema(description = "附件项") +@Data +public class AttachmentItem { + + @Schema(description = "文件编号,来自 rdms-system 文件上传返回的 data.id。Long 以字符串保存,避免前端精度丢失", + example = "10001") + private String id; + + @Schema(description = "文件访问 URL(http/https,长度 ≤ 1024)", + requiredMode = Schema.RequiredMode.REQUIRED, + example = "http://oss.example.com/task/2026/05/abc.docx") + private String url; + + @Schema(description = "原文件名(含扩展名,长度 ≤ 255)", + requiredMode = Schema.RequiredMode.REQUIRED, + example = "需求评审记录.docx") + private String name; + + @Schema(description = "文件大小(字节)", example = "245760") + private Long size; + + @Schema(description = "MIME Content-Type(建议长度 ≤ 128)", + example = "application/vnd.openxmlformats-officedocument.wordprocessingml.document") + private String contentType; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionMemberDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionAssigneeDO.java similarity index 84% rename from rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionMemberDO.java rename to rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionAssigneeDO.java index 5d9fa72..cf5a942 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionMemberDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionAssigneeDO.java @@ -9,12 +9,12 @@ import lombok.EqualsAndHashCode; import java.time.LocalDateTime; /** - * 执行成员关系表。 + * 执行协办人关系表。 */ -@TableName("rdms_execution_member") +@TableName("rdms_execution_assignee") @Data @EqualsAndHashCode(callSuper = true) -public class ExecutionMemberDO extends BaseDO { +public class ExecutionAssigneeDO extends BaseDO { /** * 主键编号 @@ -26,7 +26,7 @@ public class ExecutionMemberDO extends BaseDO { */ private Long executionId; /** - * 成员用户编号 + * 协办人用户编号 */ private Long userId; /** diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionMemberLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionAssigneeLogDO.java similarity index 87% rename from rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionMemberLogDO.java rename to rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionAssigneeLogDO.java index 0cc224f..26647b4 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionMemberLogDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/execution/ExecutionAssigneeLogDO.java @@ -9,13 +9,13 @@ import lombok.EqualsAndHashCode; import java.time.LocalDateTime; /** - * 执行成员变更历史日志(B 模型 - 全量事件记录)。 + * 执行协办人变更历史日志(B 模型 - 全量事件记录)。 * 每次 join / inactive / owner_transfer_in / owner_transfer_out 独立成一条记录,昵称展示由查询阶段按用户编号回填。 */ -@TableName("rdms_execution_member_log") +@TableName("rdms_execution_assignee_log") @Data @EqualsAndHashCode(callSuper = true) -public class ExecutionMemberLogDO extends BaseDO { +public class ExecutionAssigneeLogDO extends BaseDO { /** * 主键 ID @@ -27,7 +27,7 @@ public class ExecutionMemberLogDO extends BaseDO { */ private Long executionId; /** - * 被操作的成员用户编号 + * 被操作的协办人用户编号 */ private Long userId; /** diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/task/ProjectTaskDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/task/ProjectTaskDO.java index da72639..88d4f02 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/task/ProjectTaskDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/task/ProjectTaskDO.java @@ -1,18 +1,22 @@ package com.njcn.rdms.module.project.dal.dataobject.project.task; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem; import lombok.Data; import lombok.EqualsAndHashCode; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; /** * 项目任务主表。 */ -@TableName("rdms_task") +@TableName(value = "rdms_task", autoResultMap = true) @Data @EqualsAndHashCode(callSuper = true) public class ProjectTaskDO extends BaseDO { @@ -74,5 +78,11 @@ public class ProjectTaskDO extends BaseDO { * 最近一次状态动作原因 */ private String lastStatusReason; + /** + * 附件列表(JSON)。元素 {@link AttachmentItem}:id / url / name / size / contentType。 + * 校验由 {@code AttachmentValidator} 在 Service 入口完成。 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List attachments; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/task/TaskWorklogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/task/TaskWorklogDO.java index 1c61c9a..7dfd4cf 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/task/TaskWorklogDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/project/task/TaskWorklogDO.java @@ -1,18 +1,25 @@ package com.njcn.rdms.module.project.dal.dataobject.project.task; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem; import lombok.Data; import lombok.EqualsAndHashCode; +import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; /** - * 任务工时记录表。仅挂在叶子任务上;同一 user × task × work_date 允许多条。 - * 时长按分钟存(duration_minutes 必须 > 0 且为 30 的整数倍),前端展示为小时。 + * 任务工时记录表。仅挂在叶子任务上;按段记录(start_date/end_date 必填,单天=二者相等)。 + * 同人同任务下日期范围禁止重叠。 + * 颗粒:duration_hours 必须 > 0 且为 0.5 的整数倍。 */ -@TableName("rdms_task_worklog") +@TableName(value = "rdms_task_worklog", autoResultMap = true) @Data @EqualsAndHashCode(callSuper = true) public class TaskWorklogDO extends BaseDO { @@ -31,16 +38,37 @@ public class TaskWorklogDO extends BaseDO { */ private Long userId; /** - * 工作日期 + * 段起始日期(含),单天 = 与 endDate 相等 */ - private LocalDate workDate; + private LocalDate startDate; /** - * 时长(分钟为单位),必须 > 0 且为 30 的整数倍 + * 段结束日期(含),单天 = 与 startDate 相等 */ - private Integer durationMinutes; + private LocalDate endDate; /** - * 工作内容描述 + * 本次填报小时数,必须 > 0 且为 0.5 的整数倍 */ + private BigDecimal durationHours; + /** + * 本次填报进度(0~100,必填)。 + *

    + *
  • owner 填报:以 owner 本人最新一条工时(按 end_date desc, create_time desc, id desc)为准, + * 同步写入 {@code rdms_task.progress_rate} 并触发父任务 AVG 重算。
  • + *
  • 协作人填报:仅作为本人自评个人完成度,不影响任务/父任务进度。
  • + *
+ */ + private BigDecimal progressRate; + /** + * 工作内容描述。允许在 update 时传 null 清空(updateStrategy=ALWAYS 跳过全局 NOT_NULL 策略, + * 始终参与 SQL 拼接,包括 null)。调用方约定:update 必须全字段回传,不能用 null 表示"未改动"。 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) private String workContent; + /** + * 附件列表(JSON)。元素 {@link AttachmentItem}:id / url / name / size / contentType。 + * 校验由 {@code AttachmentValidator} 在 Service 入口完成。 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List attachments; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeLogMapper.java new file mode 100644 index 0000000..32207d3 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeLogMapper.java @@ -0,0 +1,30 @@ +package com.njcn.rdms.module.project.dal.mysql.project.execution; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogPageReqVO; +import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeLogDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ExecutionAssigneeLogMapper extends BaseMapperX { + + /** + * 分页查询执行协办人变更历史,按 actionTime DESC, id DESC 排序。 + * 支持按 actionType[] / userId / 时间范围筛选。 + */ + default PageResult selectPageByExecutionId(Long executionId, + ExecutionAssigneeLogPageReqVO reqVO) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eq(ExecutionAssigneeLogDO::getExecutionId, executionId) + .inIfPresent(ExecutionAssigneeLogDO::getActionType, reqVO.getActionTypes()) + .eqIfPresent(ExecutionAssigneeLogDO::getUserId, reqVO.getUserId()) + .geIfPresent(ExecutionAssigneeLogDO::getActionTime, reqVO.getStartTime()) + .leIfPresent(ExecutionAssigneeLogDO::getActionTime, reqVO.getEndTime()) + .orderByDesc(ExecutionAssigneeLogDO::getActionTime) + .orderByDesc(ExecutionAssigneeLogDO::getId); + return selectPage(reqVO, queryWrapper); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeMapper.java new file mode 100644 index 0000000..3fbb00a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionAssigneeMapper.java @@ -0,0 +1,70 @@ +package com.njcn.rdms.module.project.dal.mysql.project.execution; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface ExecutionAssigneeMapper extends BaseMapperX { + + default List selectListByExecutionId(Long executionId) { + return selectList(new LambdaQueryWrapperX() + .eq(ExecutionAssigneeDO::getExecutionId, executionId) + .orderByAsc(ExecutionAssigneeDO::getRemovedAt) + .orderByAsc(ExecutionAssigneeDO::getJoinedAt) + .orderByAsc(ExecutionAssigneeDO::getId)); + } + + /** + * 仅返当前活跃协办人(removed_at IS NULL)。B 模型下同 userId 至多一段未失效。 + */ + default List selectActiveListByExecutionId(Long executionId) { + return selectList(new LambdaQueryWrapperX() + .eq(ExecutionAssigneeDO::getExecutionId, executionId) + .isNull(ExecutionAssigneeDO::getRemovedAt) + .orderByAsc(ExecutionAssigneeDO::getJoinedAt) + .orderByAsc(ExecutionAssigneeDO::getId)); + } + + default ExecutionAssigneeDO selectByExecutionIdAndUserId(Long executionId, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ExecutionAssigneeDO::getExecutionId, executionId) + .eq(ExecutionAssigneeDO::getUserId, userId)); + } + + default ExecutionAssigneeDO selectByIdAndExecutionId(Long id, Long executionId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ExecutionAssigneeDO::getId, id) + .eq(ExecutionAssigneeDO::getExecutionId, executionId)); + } + + default ExecutionAssigneeDO selectActiveByExecutionIdAndUserId(Long executionId, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ExecutionAssigneeDO::getExecutionId, executionId) + .eq(ExecutionAssigneeDO::getUserId, userId) + .isNull(ExecutionAssigneeDO::getRemovedAt)); + } + + /** + * 查 userId 当前在指定项目下,活跃协办的所有执行 ID(removed_at IS NULL)。 + * 走 JOIN 是因为 execution_assignee 表没有 project_id 冗余字段。 + * 用于 VisibilityScopeResolver 收集"我是执行协办人"的 scope 来源。 + */ + @Select(""" + SELECT a.execution_id + FROM rdms_execution_assignee a + JOIN rdms_project_execution e ON e.id = a.execution_id AND e.deleted = b'0' + WHERE a.deleted = b'0' + AND a.removed_at IS NULL + AND e.project_id = #{projectId} + AND a.user_id = #{userId} + """) + List selectActiveExecutionIdsByProjectIdAndUserId(@Param("projectId") Long projectId, + @Param("userId") Long userId); + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionMemberLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionMemberLogMapper.java deleted file mode 100644 index 6157124..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionMemberLogMapper.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.njcn.rdms.module.project.dal.mysql.project.execution; - -import com.njcn.rdms.framework.common.pojo.PageResult; -import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; -import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; -import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberLogPageReqVO; -import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionMemberLogDO; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface ExecutionMemberLogMapper extends BaseMapperX { - - /** - * 分页查询执行成员变更历史,按 actionTime DESC, id DESC 排序。 - * 支持按 actionType[] / userId / 时间范围筛选。 - */ - default PageResult selectPageByExecutionId(Long executionId, - ExecutionMemberLogPageReqVO reqVO) { - LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() - .eq(ExecutionMemberLogDO::getExecutionId, executionId) - .inIfPresent(ExecutionMemberLogDO::getActionType, reqVO.getActionTypes()) - .eqIfPresent(ExecutionMemberLogDO::getUserId, reqVO.getUserId()) - .geIfPresent(ExecutionMemberLogDO::getActionTime, reqVO.getStartTime()) - .leIfPresent(ExecutionMemberLogDO::getActionTime, reqVO.getEndTime()) - .orderByDesc(ExecutionMemberLogDO::getActionTime) - .orderByDesc(ExecutionMemberLogDO::getId); - return selectPage(reqVO, queryWrapper); - } - -} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionMemberMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionMemberMapper.java deleted file mode 100644 index b3592ee..0000000 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ExecutionMemberMapper.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.njcn.rdms.module.project.dal.mysql.project.execution; - -import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; -import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; -import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionMemberDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -@Mapper -public interface ExecutionMemberMapper extends BaseMapperX { - - default List selectListByExecutionId(Long executionId) { - return selectList(new LambdaQueryWrapperX() - .eq(ExecutionMemberDO::getExecutionId, executionId) - .orderByAsc(ExecutionMemberDO::getRemovedAt) - .orderByAsc(ExecutionMemberDO::getJoinedAt) - .orderByAsc(ExecutionMemberDO::getId)); - } - - /** - * 仅返当前活跃成员(removed_at IS NULL)。B 模型下同 userId 至多一段未失效。 - */ - default List selectActiveListByExecutionId(Long executionId) { - return selectList(new LambdaQueryWrapperX() - .eq(ExecutionMemberDO::getExecutionId, executionId) - .isNull(ExecutionMemberDO::getRemovedAt) - .orderByAsc(ExecutionMemberDO::getJoinedAt) - .orderByAsc(ExecutionMemberDO::getId)); - } - - default ExecutionMemberDO selectByExecutionIdAndUserId(Long executionId, Long userId) { - return selectOne(new LambdaQueryWrapperX() - .eq(ExecutionMemberDO::getExecutionId, executionId) - .eq(ExecutionMemberDO::getUserId, userId)); - } - - default ExecutionMemberDO selectByIdAndExecutionId(Long id, Long executionId) { - return selectOne(new LambdaQueryWrapperX() - .eq(ExecutionMemberDO::getId, id) - .eq(ExecutionMemberDO::getExecutionId, executionId)); - } - - default ExecutionMemberDO selectActiveByExecutionIdAndUserId(Long executionId, Long userId) { - return selectOne(new LambdaQueryWrapperX() - .eq(ExecutionMemberDO::getExecutionId, executionId) - .eq(ExecutionMemberDO::getUserId, userId) - .isNull(ExecutionMemberDO::getRemovedAt)); - } - -} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java index 6af5bcd..1acd11e 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/execution/ProjectExecutionMapper.java @@ -7,6 +7,7 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO; import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO; import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO; +import com.njcn.rdms.module.project.service.project.permission.VisibilityScope; import org.apache.ibatis.annotations.Mapper; import org.springframework.util.StringUtils; @@ -27,21 +28,44 @@ public interface ProjectExecutionMapper extends BaseMapperX .eq(ProjectExecutionDO::getExecutionName, executionName)); } - default PageResult selectPageByProjectId(Long projectId, ProjectExecutionPageReqVO reqVO) { + default PageResult selectPageByProjectId(Long projectId, + VisibilityScope scope, + ProjectExecutionPageReqVO reqVO) { + // 可见性短路:非 seesAll 且无任何可见执行 → 空页,避免后续 IN () SQL + if (!scope.seesAll() && scope.executionIds().isEmpty()) { + return PageResult.empty(); + } LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() .eq(ProjectExecutionDO::getProjectId, projectId) .eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType()) .eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId()) .eqIfPresent(ProjectExecutionDO::getStatusCode, reqVO.getStatusCode()) .betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime()) - .orderByDesc(BaseDO::getUpdateTime) + .orderByDesc(BaseDO::getCreateTime) .orderByDesc(ProjectExecutionDO::getId); if (StringUtils.hasText(reqVO.getKeyword())) { queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword())); } + if (!scope.seesAll()) { + queryWrapper.in(ProjectExecutionDO::getId, scope.executionIds()); + } return selectPage(reqVO, queryWrapper); } + /** + * 查 userId 在指定项目下,作为 owner 的所有执行 ID。 + * 用于 VisibilityScopeResolver 收集"我是执行负责人"的 scope 来源。 + */ + default List selectIdsByProjectIdAndOwnerId(Long projectId, Long userId) { + return selectList(new LambdaQueryWrapperX() + .select(ProjectExecutionDO::getId) + .eq(ProjectExecutionDO::getProjectId, projectId) + .eq(ProjectExecutionDO::getOwnerId, userId)) + .stream() + .map(ProjectExecutionDO::getId) + .toList(); + } + default Integer countNonTerminalByProjectIdAndOwnerId(Long projectId, Long ownerId, List terminalStatusCodes) { LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() .eq(ProjectExecutionDO::getProjectId, projectId) @@ -52,7 +76,14 @@ public interface ProjectExecutionMapper extends BaseMapperX return Math.toIntExact(selectCount(queryWrapper)); } - default Integer countByProjectIdAndStatusCode(Long projectId, ProjectExecutionStatusBoardReqVO reqVO, String statusCode) { + default Integer countByProjectIdAndStatusCode(Long projectId, + VisibilityScope scope, + ProjectExecutionStatusBoardReqVO reqVO, + String statusCode) { + // 可见性短路:非 seesAll 且无任何可见执行 → 计数 0 + if (!scope.seesAll() && scope.executionIds().isEmpty()) { + return 0; + } LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() .eq(ProjectExecutionDO::getProjectId, projectId) .eq(ProjectExecutionDO::getStatusCode, statusCode) @@ -62,6 +93,9 @@ public interface ProjectExecutionMapper extends BaseMapperX if (StringUtils.hasText(reqVO.getKeyword())) { queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword())); } + if (!scope.seesAll()) { + queryWrapper.in(ProjectExecutionDO::getId, scope.executionIds()); + } return Math.toIntExact(selectCount(queryWrapper)); } @@ -74,4 +108,14 @@ public interface ProjectExecutionMapper extends BaseMapperX .eq(ProjectExecutionDO::getStatusCode, fromStatus)); } + /** + * 软删执行(按状态 CAS)。仅在 statusCode 与传入 fromStatus 匹配时才标 deleted=1。 + * 返回 1 表示成功;返回 0 视为并发修改。 + */ + default int deleteByIdAndStatus(Long id, String fromStatus) { + return delete(new LambdaQueryWrapperX() + .eq(ProjectExecutionDO::getId, id) + .eq(ProjectExecutionDO::getStatusCode, fromStatus)); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java index 48753cc..8bcb7ec 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/ProjectTaskMapper.java @@ -7,13 +7,18 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO; import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO; import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO; +import com.njcn.rdms.module.project.service.project.permission.VisibilityScope; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; @Mapper public interface ProjectTaskMapper extends BaseMapperX { @@ -25,7 +30,13 @@ public interface ProjectTaskMapper extends BaseMapperX { .eq(ProjectTaskDO::getId, taskId)); } - default PageResult selectPageByExecutionId(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) { + default PageResult selectPageByExecutionId(Long projectId, Long executionId, + VisibilityScope scope, + ProjectTaskPageReqVO reqVO) { + // 可见性短路:非 seesAll 且无任何可见任务 → 空页 + if (!scope.seesAll() && scope.taskIds().isEmpty()) { + return PageResult.empty(); + } LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); queryWrapper.eq(ProjectTaskDO::getProjectId, projectId); queryWrapper.eq(ProjectTaskDO::getExecutionId, executionId); @@ -34,11 +45,14 @@ public interface ProjectTaskMapper extends BaseMapperX { queryWrapper.eqIfPresent(ProjectTaskDO::getStatusCode, reqVO.getStatusCode()); queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime()); queryWrapper.orderByAsc(ProjectTaskDO::getParentTaskId); - queryWrapper.orderByDesc(BaseDO::getUpdateTime); + queryWrapper.orderByDesc(BaseDO::getCreateTime); queryWrapper.orderByDesc(ProjectTaskDO::getId); if (StringUtils.hasText(reqVO.getKeyword())) { queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword())); } + if (!scope.seesAll()) { + queryWrapper.in(ProjectTaskDO::getId, scope.taskIds()); + } return selectPage(reqVO, queryWrapper); } @@ -51,6 +65,16 @@ public interface ProjectTaskMapper extends BaseMapperX { .eq(ProjectTaskDO::getStatusCode, fromStatus)); } + /** + * 软删任务(按状态 CAS)。仅在 statusCode 与传入 fromStatus 匹配时才标 deleted=1。 + * 返回 1 表示成功;返回 0 视为并发修改。 + */ + default int deleteByIdAndStatus(Long id, String fromStatus) { + return delete(new LambdaQueryWrapperX() + .eq(ProjectTaskDO::getId, id) + .eq(ProjectTaskDO::getStatusCode, fromStatus)); + } + /** * 仅更新实际开始/结束日期。null 字段依据全局 FieldStrategy 不会被覆盖。 */ @@ -74,6 +98,18 @@ public interface ProjectTaskMapper extends BaseMapperX { return Math.toIntExact(selectCount(queryWrapper)); } + /** + * 统计指定执行下处于非终态的任务数。用于执行 complete 前置校验(要求所有任务必须终态)。 + */ + default Integer countByExecutionIdNotInStatus(Long executionId, List terminalStatusCodes) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eq(ProjectTaskDO::getExecutionId, executionId); + if (terminalStatusCodes != null && !terminalStatusCodes.isEmpty()) { + queryWrapper.notIn(ProjectTaskDO::getStatusCode, terminalStatusCodes); + } + return Math.toIntExact(selectCount(queryWrapper)); + } + /** * 数指定父任务下的直接子任务(不区分状态)。用于"是否叶子任务"判定与进度汇总。 */ @@ -98,6 +134,39 @@ public interface ProjectTaskMapper extends BaseMapperX { .eq(ProjectTaskDO::getParentTaskId, parentTaskId)); } + /** + * 执行详情进度:按当前执行下一级任务 progressRate 简单平均;无一级任务时 SQL 返回 null。 + */ + @Select(""" + SELECT AVG(COALESCE(progress_rate, 0)) + FROM rdms_task + WHERE deleted = b'0' + AND project_id = #{projectId} + AND execution_id = #{executionId} + AND parent_task_id IS NULL + """) + BigDecimal selectRootTaskAvgProgressByExecutionId(@Param("projectId") Long projectId, + @Param("executionId") Long executionId); + + /** + * 执行分页进度:按当前页 executionId 批量聚合一级任务 progressRate,避免列表 N+1。 + */ + @Select(""" + + """) + List> selectRootTaskAvgProgressGroupByExecutionIds( + @Param("projectId") Long projectId, + @Param("executionIds") Collection executionIds); + /** * 仅更新单个任务的 progressRate,不动其他字段(避免污染 lastStatusReason 等)。 */ @@ -108,9 +177,62 @@ public interface ProjectTaskMapper extends BaseMapperX { .eq(ProjectTaskDO::getId, id)); } + /** + * 递归 CTE:从"userId 在指定项目下作为 owner_id 的全部任务"出发,向下展开包含所有子孙的任务 ID。 + * 用于 VisibilityScopeResolver 中"任务负责人 → 自己 + 全部子孙"规则的 scope 收集。 + * + * 任务表已逻辑删除的行不参与递归(WHERE 子句过滤 deleted)。 + * 单棵子树最大深度受 MySQL `cte_max_recursion_depth`(默认 1000)限制,业务实际任务树远低于此。 + */ + @Select(""" + WITH RECURSIVE owned (id) AS ( + SELECT id FROM rdms_task + WHERE deleted = b'0' + AND project_id = #{projectId} + AND owner_id = #{userId} + UNION ALL + SELECT t.id FROM rdms_task t + JOIN owned o ON t.parent_task_id = o.id + WHERE t.deleted = b'0' + ) + SELECT id FROM owned + """) + List selectOwnedTaskAndDescendantIdsByProjectIdAndUserId( + @Param("projectId") Long projectId, + @Param("userId") Long userId); + + /** + * 同 selectOwnedTaskAndDescendantIdsByProjectIdAndUserId 但再加 execution_id 维度。 + * 注意:递归向下展开只跟着 parent_task_id,子任务必然与父任务在同一 execution 下, + * 因此 execution_id 过滤仅作用于种子(owned)那一步即可。 + */ + @Select(""" + WITH RECURSIVE owned (id) AS ( + SELECT id FROM rdms_task + WHERE deleted = b'0' + AND project_id = #{projectId} + AND execution_id = #{executionId} + AND owner_id = #{userId} + UNION ALL + SELECT t.id FROM rdms_task t + JOIN owned o ON t.parent_task_id = o.id + WHERE t.deleted = b'0' + ) + SELECT id FROM owned + """) + List selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId( + @Param("projectId") Long projectId, + @Param("executionId") Long executionId, + @Param("userId") Long userId); + default Integer countByProjectIdAndExecutionIdAndStatusCode(Long projectId, Long executionId, + VisibilityScope scope, ProjectTaskStatusBoardReqVO reqVO, String statusCode) { + // 可见性短路:非 seesAll 且无任何可见任务 → 0 + if (!scope.seesAll() && scope.taskIds().isEmpty()) { + return 0; + } LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() .eq(ProjectTaskDO::getProjectId, projectId) .eq(ProjectTaskDO::getExecutionId, executionId) @@ -121,6 +243,9 @@ public interface ProjectTaskMapper extends BaseMapperX { if (StringUtils.hasText(reqVO.getKeyword())) { queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword())); } + if (!scope.seesAll()) { + queryWrapper.in(ProjectTaskDO::getId, scope.taskIds()); + } return Math.toIntExact(selectCount(queryWrapper)); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskAssigneeMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskAssigneeMapper.java index e864735..0cfc1ce 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskAssigneeMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskAssigneeMapper.java @@ -4,6 +4,8 @@ import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import java.util.Collection; import java.util.Collections; @@ -48,6 +50,41 @@ public interface TaskAssigneeMapper extends BaseMapperX { .orderByAsc(TaskAssigneeDO::getId)); } + /** + * 查 userId 在指定项目下,当前活跃协办的所有任务 ID(removed_at IS NULL)。 + * 走 JOIN 是因为 task_assignee 表没有 project_id 冗余字段。 + * 用于 VisibilityScopeResolver 收集"我是任务协办人"的 scope 来源(项目维度)。 + */ + @Select(""" + SELECT a.task_id + FROM rdms_task_assignee a + JOIN rdms_task t ON t.id = a.task_id AND t.deleted = b'0' + WHERE a.deleted = b'0' + AND a.removed_at IS NULL + AND t.project_id = #{projectId} + AND a.user_id = #{userId} + """) + List selectActiveTaskIdsByProjectIdAndUserId(@Param("projectId") Long projectId, + @Param("userId") Long userId); + + /** + * 同上,但再加 execution_id 维度,用于"任务分页(执行内)"的 scope。 + */ + @Select(""" + SELECT a.task_id + FROM rdms_task_assignee a + JOIN rdms_task t ON t.id = a.task_id AND t.deleted = b'0' + WHERE a.deleted = b'0' + AND a.removed_at IS NULL + AND t.project_id = #{projectId} + AND t.execution_id = #{executionId} + AND a.user_id = #{userId} + """) + List selectActiveTaskIdsByProjectIdAndExecutionIdAndUserId( + @Param("projectId") Long projectId, + @Param("executionId") Long executionId, + @Param("userId") Long userId); + /** * 按主键 + 任务 ID 双键查;返回的记录可能已失效(removed_at != null),由调用方判断。 */ diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java index d0dca64..f3badfc 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/task/TaskWorklogMapper.java @@ -9,6 +9,8 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import java.math.BigDecimal; +import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.Map; @@ -27,35 +29,40 @@ public interface TaskWorklogMapper extends BaseMapperX { } /** - * 任务工时分页:按 workDate DESC, id DESC;支持按填报人 / 日期区间筛选。 + * 任务工时分页:按 endDate DESC, id DESC;支持按填报人 / 段相交过滤。 + * 段相交语义:record.startDate <= filter.endDate AND record.endDate >= filter.startDate。 */ default PageResult selectPageByTaskId(Long taskId, TaskWorklogPageReqVO reqVO) { LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() .eq(TaskWorklogDO::getTaskId, taskId) - .eqIfPresent(TaskWorklogDO::getUserId, reqVO.getUserId()) - .geIfPresent(TaskWorklogDO::getWorkDate, reqVO.getStartDate()) - .leIfPresent(TaskWorklogDO::getWorkDate, reqVO.getEndDate()) - .orderByDesc(TaskWorklogDO::getWorkDate) + .eqIfPresent(TaskWorklogDO::getUserId, reqVO.getUserId()); + if (reqVO.getEndDate() != null) { + queryWrapper.le(TaskWorklogDO::getStartDate, reqVO.getEndDate()); + } + if (reqVO.getStartDate() != null) { + queryWrapper.ge(TaskWorklogDO::getEndDate, reqVO.getStartDate()); + } + queryWrapper.orderByDesc(TaskWorklogDO::getEndDate) .orderByDesc(TaskWorklogDO::getId); return selectPage(reqVO, queryWrapper); } /** - * 单任务工时汇总(分钟)。无记录时返回 0;逻辑删除的记录不参与汇总。 + * 单任务工时小时数汇总。无记录时返回 0;逻辑删除的记录不参与汇总。 */ @Select(""" - SELECT COALESCE(SUM(duration_minutes), 0) + SELECT COALESCE(SUM(duration_hours), 0) FROM rdms_task_worklog WHERE deleted = b'0' AND task_id = #{taskId} """) - Long sumDurationByTaskId(@Param("taskId") Long taskId); + BigDecimal sumDurationByTaskId(@Param("taskId") Long taskId); /** - * 批量任务工时汇总(分钟),返回 [{taskId, total}]。用于详情/分页装配避免 N+1。 + * 批量任务工时小时数汇总,返回 [{taskId, total}]。用于详情/分页装配避免 N+1。 */ @Select("""