Compare commits
14 Commits
9e4f8becc8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 622b30733e | |||
| f13286aaff | |||
| f23f1930e9 | |||
| 55a50eb3d5 | |||
|
|
679edf08ba | ||
| e71140d8a2 | |||
| 5c7dbf7286 | |||
| 9f03dc27cc | |||
| d669d53a80 | |||
| df13a90107 | |||
| 8a36b49128 | |||
| c9549bed46 | |||
| 5caf3bbdc9 | |||
| 58eed8234a |
@@ -105,7 +105,14 @@
|
|||||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; $out = & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests 2>&1; $code = $LASTEXITCODE; $out | Select-Object -Last 15; Write-Output \"EXIT=$code\")",
|
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; $out = & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests 2>&1; $code = $LASTEXITCODE; $out | Select-Object -Last 15; Write-Output \"EXIT=$code\")",
|
||||||
"Bash(set \"JAVA_HOME=C:\\\\Program Files\\\\Java\\\\jdk-17\")",
|
"Bash(set \"JAVA_HOME=C:\\\\Program Files\\\\Java\\\\jdk-17\")",
|
||||||
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
|
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
|
||||||
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)"
|
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)",
|
||||||
|
"Bash(grep -E \"\\\\.\\(sql|java|md\\)$\")",
|
||||||
|
"Bash(xargs grep -l \"INSERT INTO.*system_menu\")",
|
||||||
|
"Bash(Get-ChildItem *)",
|
||||||
|
"Bash(Select-Object FullName)",
|
||||||
|
"PowerShell($env:JAVA_HOME='C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -e -pl rdms-project/rdms-project-boot test \"-Dtest=ProjectExecutionServiceImplTest#changeExecutionStatus_whenCompleteTransitionMissing_shouldThrow\" 2>&1 | Select-String -Pattern \"BUILD|Tests run|FAIL|ERROR|passed\" | Select-Object -First 20)",
|
||||||
|
"PowerShell($env:JAVA_HOME='C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -q -pl rdms-project/rdms-project-boot -am compile 2>&1 | Select-Object -Last 20)",
|
||||||
|
"PowerShell($env:JAVA_HOME='C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -e -pl rdms-project/rdms-project-boot test -Dtest=ProjectServiceImplTest 2>&1 | Select-Object -Last 40)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
CLAUDE.md
63
CLAUDE.md
@@ -9,12 +9,14 @@
|
|||||||
- 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。
|
- 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。
|
||||||
- **输出极简**:先给结论、改动点、必要风险;用自然语言给判断和影响面,少贴代码片段;涉及代码用 `file_path:line_number` 引用;用户追问再展开。
|
- **输出极简**:先给结论、改动点、必要风险;用自然语言给判断和影响面,少贴代码片段;涉及代码用 `file_path:line_number` 引用;用户追问再展开。
|
||||||
- **下定论需要充足证据**。疑似 bug 时先判断是否稳定复现:跑了很久没动过的功能**首次**报错,优先怀疑运行时状态污染(devtools / IDE 热替换、ApplicationContext 残留、缓存、Redis / DB 连接、JVM 静态字段被旧 context 设过等),**不要凭单次堆栈就断言代码 bug,更不要直接甩修改方案**。先给"可能原因 + 最便宜的取证步骤"(多数场景是**冷重启 JVM**),用户确认能稳定复现,再讨论代码层面的修复。同款写法在仓库其它位置存在并不能反推"也是 bug",长期能跑的代码突然失效 ≠ 代码本身错。
|
- **下定论需要充足证据**。疑似 bug 时先判断是否稳定复现:跑了很久没动过的功能**首次**报错,优先怀疑运行时状态污染(devtools / IDE 热替换、ApplicationContext 残留、缓存、Redis / DB 连接、JVM 静态字段被旧 context 设过等),**不要凭单次堆栈就断言代码 bug,更不要直接甩修改方案**。先给"可能原因 + 最便宜的取证步骤"(多数场景是**冷重启 JVM**),用户确认能稳定复现,再讨论代码层面的修复。同款写法在仓库其它位置存在并不能反推"也是 bug",长期能跑的代码突然失效 ≠ 代码本身错。
|
||||||
|
- **技术风险判断(性能 / N+1 / 索引缺失 / 架构缺陷 / 并发安全 / 内存泄漏 等)与"bug 判断"同等严格**:未读到实现层不下结论。不要凭 subagent 摘要、字段名、注释或印象"顺嘴提一句风险/瓶颈/可能问题"——那也是下结论,**且杀伤力更大**:用户会基于"风险提示"决定要不要立项整改。如果当前上下文没核实到实现,就明说"这部分未核实,需要打开 X 文件确认",不要把猜测包装成"风险提示"塞出去。已识别教训:执行进度查询答完"已批量聚合无 N+1"后又凭印象抛"列表 N+1 风险",被追问才收回。
|
||||||
|
|
||||||
## 本机环境
|
## 本机环境
|
||||||
|
|
||||||
- JDK:必须使用 `JDK 17`,路径 `C:\Program Files\Java\jdk-17`。不要使用 JDK 8 / 11 / 其他版本。
|
- JDK:必须使用 `JDK 17`,路径 `C:\Program Files\Java\jdk-17`。不要使用 JDK 8 / 11 / 其他版本。
|
||||||
- Maven:`C:\software\apache-maven-3.8.9`,命令优先用完整路径 `C:\software\apache-maven-3.8.9\bin\mvn.cmd`。不要假设 `mvn` 在 PATH。
|
- Maven:`C:\software\apache-maven-3.8.9`,命令优先用完整路径 `C:\software\apache-maven-3.8.9\bin\mvn.cmd`。不要假设 `mvn` 在 PATH。
|
||||||
- 执行任何 Maven / java 命令前,先确认当前 shell 的 `JAVA_HOME` 指向 JDK 17,且 `java -version` 输出 17;否则在该命令上下文中显式切换。
|
- 执行任何 Maven / java 命令前,先确认当前 shell 的 `JAVA_HOME` 指向 JDK 17,且 `java -version` 输出 17;否则在该命令上下文中显式切换。
|
||||||
|
- MySQL 客户端:本机已装命令行客户端 `C:\software\mysql-8.4.9-winx64\bin\mysql.exe`,Agent 可直接连库跑 SQL(查数据、核对状态、验证库结构),无需另装工具。连接账密**不写进本文件**,以 `application-local.yaml` 的 `spring.datasource` 段为准(开发库 `192.168.1.22:13306`/`rdms_view`);**演示库是另一套,账密以用户当场给的为准**。**读随便跑;写库(DDL/DML)务必先确认是哪套库**,开发库改动按本文件§数据与 SQL 出演示库补丁,绝不擅自改演示库。调用细节(PowerShell 必须逐参数加单引号否则 host 被拆、字符集、常用查询)见 [`docs/agent/MySQL客户端连库手册.md`](./docs/agent/MySQL客户端连库手册.md)。
|
||||||
|
|
||||||
## 仓库结构
|
## 仓库结构
|
||||||
|
|
||||||
@@ -47,7 +49,6 @@
|
|||||||
不要:
|
不要:
|
||||||
- 把后续业务长期堆进 `rdms-system`。
|
- 把后续业务长期堆进 `rdms-system`。
|
||||||
- 为新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层。
|
- 为新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层。
|
||||||
- 让外部模块直接依赖 `*-boot` 的 service 或 mapper(必须走 `*-api`)。
|
|
||||||
|
|
||||||
## 分层职责
|
## 分层职责
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
## 认证与跨模块调用
|
## 认证与跨模块调用
|
||||||
|
|
||||||
- 默认沿用 OAuth2 / Token / `LoginUser` / `login-user` 透传主链。**不要**另造 ThreadLocal / Session / 自定义 header。
|
- 默认沿用 OAuth2 / Token / `LoginUser` / `login-user` 透传主链。**不要**另造 ThreadLocal / Session / 自定义 header。
|
||||||
- 业务逻辑落 `*-boot`;可复用契约落 `*-api`;可复用框架能力落 `rdms-framework`。跨模块/跨服务必须通过 `*-api` 定义契约,**不要直接依赖别人的 `*-boot`**;改跨模块 API 时,`*-boot` 实现与对应 `*-api` 契约同步更新。
|
- 业务逻辑落 `*-boot`;可复用契约落 `*-api`;可复用框架能力落 `rdms-framework`。跨模块/跨服务必须通过 `*-api` 定义契约,**不要直接依赖别人 `*-boot` 的 service 或 mapper(必须走 `*-api`)**;改跨模块 API 时,`*-boot` 实现与对应 `*-api` 契约同步更新。
|
||||||
|
|
||||||
### 鉴权:必须按"全域 / 对象域"分通道挂
|
### 鉴权:必须按"全域 / 对象域"分通道挂
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。
|
- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。
|
||||||
- **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。
|
- **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。
|
||||||
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
|
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
|
||||||
- 列表/详情这类对象内**读路径**目前未挂 `@CheckObjectPermission`(属已识别负债,台账 TD-001),新增读接口暂沿用现状即可,不要顺手改造,等独立立项。
|
- 对象内**读路径**(列表 / 详情 / 状态看板 / 聚合)同样要挂 `@CheckObjectPermission(objectType=PROJECT, permission=...PERMISSION_QUERY)`——查询同样扫库耗资源,必须按对象域鉴权。**Controller 方法层一律不挂权限注解**,新增读接口照此在 Service 层挂对象域权限;不要只在 Controller 留空,更不要误判"Controller 没注解 = 无鉴权"。
|
||||||
|
|
||||||
判定口诀:**URL 里有 `{projectId}` / `{productId}` 等对象 ID → 对象域;没有 → 全域**。
|
判定口诀:**URL 里有 `{projectId}` / `{productId}` 等对象 ID → 对象域;没有 → 全域**。
|
||||||
|
|
||||||
@@ -99,8 +100,32 @@
|
|||||||
- 新增 update 接口时,必须在 API 文档对应章节明示"PUT 全字段回传"约定;DO 上对允许 null 的字段补 `FieldStrategy.ALWAYS` 注解,并加注释说明语义来源(指向本节)。
|
- 新增 update 接口时,必须在 API 文档对应章节明示"PUT 全字段回传"约定;DO 上对允许 null 的字段补 `FieldStrategy.ALWAYS` 注解,并加注释说明语义来源(指向本节)。
|
||||||
- 历史接口若是稀疏 PATCH 风格(传 null = 不动),保留现状但不要拓展;遇到清空诉求时按 PUT 方向重构。
|
- 历史接口若是稀疏 PATCH 风格(传 null = 不动),保留现状但不要拓展;遇到清空诉求时按 PUT 方向重构。
|
||||||
|
|
||||||
|
## 用户可见错误文案(状态机动作 / 状态校验)
|
||||||
|
|
||||||
|
凡"状态机动作 / 状态校验失败"的业务异常,若 `message` 会被前端直接 toast 给用户:**不要把动作 / 状态的内部 code 塞进 message**(如"不支持动作【complete】")。给用户看中文展示名,技术诊断(原始 code / 入参 / 堆栈)由 `infra_api_access_log` 承载。
|
||||||
|
|
||||||
|
红线:
|
||||||
|
|
||||||
|
- service 注入 `StatusActionTextResolver`(`rdms-project-boot · service/status`),抛错前用 `actionName / statusName` 把 code 翻成中文名再填错误码占位;中文名权威源是 `rdms_object_status_transition.action_name` / `rdms_object_status_model.status_name`,**不在代码里硬编码 code→中文 映射**。
|
||||||
|
- 错误码文案用「{}」占位中文名(正面样板 `PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED`),不要把 code 写死进文案。
|
||||||
|
- 新对象类型只要在状态机两张表配好 `status_name` / `action_name`,resolver 自动生效,无需改代码。
|
||||||
|
|
||||||
|
完整规范、落地清单与已知缺口(加班申请 `OvertimeApplicationServiceImpl` 待补)见 [`docs/architecture/用户可见错误文案规范.html`](./docs/architecture/用户可见错误文案规范.html)。来源 TD-012。
|
||||||
|
|
||||||
## 数据与 SQL
|
## 数据与 SQL
|
||||||
|
|
||||||
|
### 🔴 演示库同步补丁(上线前最高优先级,每次开发都要做)
|
||||||
|
|
||||||
|
项目已临近上线测试,**开发库与演示库两套数据库并存**:日常开发只改了开发库,演示库要靠 SQL 补丁手工同步。一旦补丁缺失或漏项,演示库就会因数据结构 / 字典 / 权限缺失导致功能异常。因此——
|
||||||
|
|
||||||
|
- **每次开发引起的任何数据库变动,都必须同步产出一个可直接运行的 SQL 补丁文件**,交给用户拿到演示库执行。覆盖范围:表结构(DDL)、数据字典(`system_dict_type` / `system_dict_data`)、菜单 / 角色 / 权限(`system_menu` 及权限相关表)、新功能初始化数据、索引等——**只要开发库动了,补丁就必须有**。
|
||||||
|
- **落地位置与命名**:`docs/sql/patches/`,命名 `YYYY-MM-DD-功能名-NN.sql`(每次变更一个独立文件;当天同一功能多次变更递增 `NN` 序号)。沿用 `docs/sql/**` 不主动提交 git 的惯例。
|
||||||
|
- **可直接执行**:纯 SQL、自包含,不依赖 IDE / 迁移工具;用户复制即可在演示库一次跑通。
|
||||||
|
- **幂等可重复**:演示库可能已有部分数据,补丁必须可重复执行不报错(`NOT EXISTS` / `IF NOT EXISTS` 守卫;雪花 id 表按下文「种子 SQL」规则显式取 id)。
|
||||||
|
- **只含增量**:仅本次变动,不 dump 全库、不夹带无关数据。
|
||||||
|
- 补丁写法严格遵守下文「种子 SQL」小节(雪花 id 取值、collation 1267 陷阱)。
|
||||||
|
- **收尾必做**:完成任何涉及 DB 的开发后,主动告诉用户「本次演示库补丁:`docs/sql/patches/xxx.sql`」并简述其内容,不要等用户来问。
|
||||||
|
|
||||||
- 新表 DO 复用现有 `BaseDO` / 审计字段风格,不要再引一套审计基类(除非该表本身明确不需要逻辑删除)。
|
- 新表 DO 复用现有 `BaseDO` / 审计字段风格,不要再引一套审计基类(除非该表本身明确不需要逻辑删除)。
|
||||||
- **不要假设运行时自动数据库迁移**:依赖新表/新字段/新索引时,必须同步补 SQL 脚本与文档。
|
- **不要假设运行时自动数据库迁移**:依赖新表/新字段/新索引时,必须同步补 SQL 脚本与文档。
|
||||||
- SQL 放在目标模块 `src/main/resources/sql/...`,可审阅、可单独执行。
|
- SQL 放在目标模块 `src/main/resources/sql/...`,可审阅、可单独执行。
|
||||||
@@ -108,13 +133,13 @@
|
|||||||
|
|
||||||
### 种子 SQL(纯 SQL INSERT 雪花 ID 表)
|
### 种子 SQL(纯 SQL INSERT 雪花 ID 表)
|
||||||
|
|
||||||
`system_dict_type` / `system_dict_data` / `system_menu` 等历史表 id 由 MyBatis-Plus 雪花算法在 Java 层生成,DDL 无 `AUTO_INCREMENT`。纯 SQL 路径(字典种子、菜单种子等)写 INSERT 必须显式提供 id,否则 MySQL 报 `1364 - Field 'id' doesn't have a default value`。
|
`system_dict_type` / `system_dict_data` / `system_menu` 等历史表 id 由 MyBatis-Plus 雪花算法在 Java 层生成,DDL 无 `AUTO_INCREMENT`。纯 SQL 路径(字典种子、菜单种子、演示库补丁)写 INSERT 必须遵守三条红线,否则报错或撞库:
|
||||||
|
|
||||||
- **id 取值**:`SET @new_id = (SELECT IFNULL(MAX(id), 0) + 1 FROM xxx_table);` 然后 INSERT `SELECT @new_id, ...`。雪花 ID 单调递增,`MAX+1` 落在已用区间之后,不会与未来 Java 生成的新雪花 ID 冲突。
|
- **必须显式给 id**:用 `MAX+1` 取(`SET @new_id = (SELECT IFNULL(MAX(id),0)+1 FROM xxx);`),多条连续 INSERT **每条前重新取**,否则 `1364 - Field 'id' doesn't have a default value`。
|
||||||
- **多条连续 INSERT**:每条 INSERT **前重新取** `MAX+1`——不要用 `base+1 / +2 / +3` 一次性算多个。配合 `NOT EXISTS` 守卫,部分已存在场景(半路重跑)才不会出现两条共用一个 id。
|
- **必须幂等**:每条 INSERT 加 `NOT EXISTS` 守卫,可重复执行不重插。
|
||||||
- **collation 1267 陷阱**:仓库历史表 collation 不统一(如 `system_dict_data` 是 `utf8mb4_unicode_ci`,新表 `rdms_task_worklog` 是 `utf8mb4_0900_ai_ci`)。**不要**用 `SET @t = 'xxx'` 存字符串再 `WHERE col = @t`——用户变量自带连接级 collation,与列 collation 撞会报 `1267 Illegal mix of collations (utf8mb4_unicode_ci,IMPLICIT) and (utf8mb4_0900_ai_ci,IMPLICIT) for operation '='`。**对策**:直接展开成字面值,MySQL 字面值会按列 collation 隐式解析,不冲突。
|
- **防 collation 1267**:**不要**用用户变量存字符串再比较(`SET @t='xxx'` 后 `WHERE col=@t`),仓库历史表 collation 不统一会撞 `1267 Illegal mix of collations`;直接展开成字面值。
|
||||||
|
|
||||||
样板参考:`docs/sql/rdms_task_worklog.sql:47-50`(菜单种子)+ `docs/sql/rdms_worklog_difficulty_seed.sql`(字典种子)。
|
详细写法、示例与样板(`docs/sql/rdms_project_query_permission.sql`)见 [`docs/agent/种子SQL写法规范.md`](./docs/agent/种子SQL写法规范.md)。
|
||||||
|
|
||||||
## 注释与编码
|
## 注释与编码
|
||||||
|
|
||||||
@@ -124,9 +149,29 @@
|
|||||||
## 文档输出格式
|
## 文档输出格式
|
||||||
|
|
||||||
- 新写文档默认输出 **HTML 格式**(便于浏览器直接打开、自带样式)。
|
- 新写文档默认输出 **HTML 格式**(便于浏览器直接打开、自带样式)。
|
||||||
- 例外:`docs/superpowers/` 下保持 markdown(工作流约定)。
|
- 例外:`docs/superpowers/` 与 `docs/agent/` 下保持 **markdown**——前者是工作流约定,后者是专给 Agent 看的操作手册(Agent 读 md 更直接,不需要浏览器样式)。
|
||||||
- 历史已有的 markdown 文档不强制迁移;只有新写的按 HTML。
|
- 历史已有的 markdown 文档不强制迁移;只有新写的按 HTML。
|
||||||
|
|
||||||
|
### 产出落点(每次生成前对照,避免散乱)
|
||||||
|
|
||||||
|
**红线:绝不把生成物放在仓库根或 `docs/` 根目录**——任何文档 / SQL / 草稿必须落到 `docs/` 下对应子目录(完整目录见 [`docs/README.md`](./docs/README.md) 场景表)。历史教训:工作台 API 文档被丢到仓库根 + docs 根导致散乱。
|
||||||
|
|
||||||
|
| 产出类型 | 落点 |
|
||||||
|
|---|---|
|
||||||
|
| 业务功能文档(设计 / 前端 API / 实现说明 / 联调) | `docs/domains/<域>/`(域=system/product/project),**平铺**、命名 `YYYY-MM-DD-功能-类型.html`,**不按功能建子目录** |
|
||||||
|
| superpowers 工作流产物(spec / plan) | `docs/superpowers/specs/` `plans/`(保持 markdown) |
|
||||||
|
| 跨模块约束 / 架构规范 | `docs/architecture/` |
|
||||||
|
| starter / 框架 API 说明 | `docs/modules/` |
|
||||||
|
| 专给 Agent 看的操作手册(从 CLAUDE.md 抽出的长篇 how-to / 命令 / 陷阱) | `docs/agent/`(CLAUDE.md 留红线 + 指针) |
|
||||||
|
| 调研 / 对标 / 可行性 | `docs/research/` |
|
||||||
|
| 负债台账 / 排查经验 | `docs/debt/` |
|
||||||
|
| 演示库同步补丁 | `docs/sql/patches/`(见本文件§数据与 SQL) |
|
||||||
|
| SQL 草稿 | `docs/sql/`(最终迁回模块 `src/main/resources/sql/`) |
|
||||||
|
| **过程草稿 / 待确认清单 / "看一眼"稿** | **`docs/temp/`,定稿后及时删** |
|
||||||
|
|
||||||
|
- 正文格式按上文「文档输出格式」(默认 HTML,`superpowers/` 与 `docs/agent/` 保持 md,历史 md 不强迁)。
|
||||||
|
- 落点对应的子目录有 `README.md` 时,新增文件**同步登记**进该 README 的文件表。
|
||||||
|
|
||||||
## 工作规则(执行前对照)
|
## 工作规则(执行前对照)
|
||||||
|
|
||||||
1. 优先做有边界的模块内改动,避免跨模块扩散。
|
1. 优先做有边界的模块内改动,避免跨模块扩散。
|
||||||
|
|||||||
@@ -1,355 +0,0 @@
|
|||||||
# 工单需求规格说明
|
|
||||||
|
|
||||||
日期:2026-05-22
|
|
||||||
|
|
||||||
## 1. 背景
|
|
||||||
|
|
||||||
`rdms-project` 当前承载项目、产品、需求、执行、任务等核心交付对象。现有代码中产品需求、项目需求已经具备 `sourceType` / `sourceBizId` 来源字段,可以承接来自工单的需求派生关系;执行和任务也已经形成“项目需求 -> 执行 -> 任务”的后续交付链路。
|
|
||||||
|
|
||||||
本需求新增内部工单能力。工单作为独立业务对象存在,不复用需求、执行或任务主表。工单用于记录内部用户提交的诉求,经工单负责人受理后,可按归属类型派生产品需求或项目需求,并通过现有需求链路继续流转到执行、任务。
|
|
||||||
|
|
||||||
## 2. 目标
|
|
||||||
|
|
||||||
1. 支持内部用户创建普通工单或父工单。
|
|
||||||
2. 支持父工单逐步拆分子工单,父工单只汇总,不直接处理。
|
|
||||||
3. 支持普通工单、子工单作为最小处理单位,由指定工单负责人受理、拒绝、处理和关闭。
|
|
||||||
4. 支持工单单归属到一个产品或一个项目。
|
|
||||||
5. 支持产品工单派生产品需求、项目工单派生项目需求。
|
|
||||||
6. 支持有派生需求的工单在全部需求完成后自动关闭。
|
|
||||||
7. 支持无派生需求的工单由工单负责人手动关闭。
|
|
||||||
|
|
||||||
## 3. 非目标
|
|
||||||
|
|
||||||
1. 本期不做外部客户工单,不保留 `sourceChannel`、`externalCustomerName`、`externalContact` 等外部来源字段。
|
|
||||||
2. 本期不做工单编号 `ticketNo`。
|
|
||||||
3. 工单不能直接派生执行或任务。
|
|
||||||
4. 父工单不能受理、拒绝、派生需求或手动关闭。
|
|
||||||
5. 本期不引入流程引擎,不做可配置审批流。
|
|
||||||
6. 本期不自动判断工单是否涉及多个产品/项目,也不自动判断归属产品或项目;这些由录入人员人工选择。
|
|
||||||
|
|
||||||
## 4. 核心概念
|
|
||||||
|
|
||||||
### 4.1 工单形态
|
|
||||||
|
|
||||||
使用 `ticketMode` 表达工单形态:
|
|
||||||
|
|
||||||
| 值 | 含义 | 是否可处理 | 说明 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `single` | 普通工单 | 是 | 单归属工单,不挂父工单 |
|
|
||||||
| `parent` | 父工单 | 否 | 原始诉求汇总单,可持续拆分子工单 |
|
|
||||||
| `child` | 子工单 | 是 | 挂在父工单下的最小处理单位 |
|
|
||||||
|
|
||||||
不使用 `isParent`。`ticketMode` 比布尔字段更准确,可以区分普通工单、父工单和子工单,避免在父工单尚未创建子工单时无法识别其形态。
|
|
||||||
|
|
||||||
### 4.2 归属类型
|
|
||||||
|
|
||||||
普通工单和子工单必须单归属:
|
|
||||||
|
|
||||||
| `belongType` | 归属对象 | 可派生对象 | 后续链路 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `product` | 一个产品 | 产品需求 | 产品需求 -> 指派项目 -> 项目需求 -> 执行 -> 任务 |
|
|
||||||
| `project` | 一个项目 | 项目需求 | 项目需求 -> 执行 -> 任务 |
|
|
||||||
|
|
||||||
父工单不填写 `belongType`、`productId`、`projectId`。
|
|
||||||
|
|
||||||
## 5. 业务流程
|
|
||||||
|
|
||||||
### 5.1 总流程图
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A([开始]) --> B[录入人员创建工单]
|
|
||||||
B --> C{ticketMode}
|
|
||||||
|
|
||||||
C -->|parent| P1[父工单: splitting]
|
|
||||||
P1 --> P2[列表操作列拆分子工单]
|
|
||||||
P2 --> C1[创建子工单: pending_accept]
|
|
||||||
P1 --> P3{是否存在子工单且全部为终态}
|
|
||||||
P3 -->|否| P1
|
|
||||||
P3 -->|是| P4[父工单: closed]
|
|
||||||
P4 --> Z([结束])
|
|
||||||
|
|
||||||
C -->|single| S1[普通工单: pending_accept]
|
|
||||||
C1 --> S2[工单负责人待处理]
|
|
||||||
S1 --> S2
|
|
||||||
S2 --> D{是否受理}
|
|
||||||
D -->|否| R[工单: rejected]
|
|
||||||
R --> Z
|
|
||||||
D -->|是| E[工单: processing]
|
|
||||||
|
|
||||||
E --> F{是否派生需求}
|
|
||||||
F -->|否| G[负责人填写处理结论并手动关闭]
|
|
||||||
G --> H[工单: closed]
|
|
||||||
H --> I[触发父工单汇总检查]
|
|
||||||
I --> Z
|
|
||||||
|
|
||||||
F -->|是| J{归属类型}
|
|
||||||
J -->|product| K[派生一个或多个产品需求]
|
|
||||||
J -->|project| L[派生一个或多个项目需求]
|
|
||||||
K --> M[等待派生需求全部完成]
|
|
||||||
L --> M
|
|
||||||
M --> N{全部派生需求完成}
|
|
||||||
N -->|否| E
|
|
||||||
N -->|是| O[系统自动关闭工单: closed]
|
|
||||||
O --> I
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 父工单流程
|
|
||||||
|
|
||||||
1. 录入人员创建父工单,状态为 `splitting`。
|
|
||||||
2. 父工单只记录原始诉求和附件,不进入处理队列。
|
|
||||||
3. 工单列表查询的操作列为父工单提供“拆分子工单”入口。
|
|
||||||
4. 录入人员可以持续新增子工单。
|
|
||||||
5. 父工单至少存在一个子工单,且所有子工单均进入终态后,系统自动关闭父工单。
|
|
||||||
6. 父工单关闭后不再作为处理对象,但可继续作为历史汇总查看。
|
|
||||||
|
|
||||||
### 5.3 普通工单 / 子工单流程
|
|
||||||
|
|
||||||
1. 创建后进入 `pending_accept`。
|
|
||||||
2. 工单负责人判断是否受理。
|
|
||||||
3. 不受理则进入 `rejected`,需要填写拒绝原因。
|
|
||||||
4. 受理后进入 `processing`。
|
|
||||||
5. 处理中可以派生需求,也可以在无派生需求时填写处理结论并手动关闭。
|
|
||||||
6. 一旦存在派生需求,工单不能手动关闭,必须等待全部派生需求完成后自动关闭。
|
|
||||||
|
|
||||||
## 6. 状态模型
|
|
||||||
|
|
||||||
### 6.1 父工单状态
|
|
||||||
|
|
||||||
| 状态 | 含义 | 进入方式 | 退出方式 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `splitting` | 拆分中 / 汇总中 | 创建父工单 | 所有子工单终态后自动关闭 |
|
|
||||||
| `closed` | 已关闭 | 系统自动关闭 | 终态 |
|
|
||||||
|
|
||||||
父工单不允许进入 `pending_accept`、`rejected`、`processing`。
|
|
||||||
|
|
||||||
### 6.2 普通工单 / 子工单状态
|
|
||||||
|
|
||||||
| 状态 | 含义 | 进入方式 | 退出方式 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `pending_accept` | 待受理 | 创建普通工单或子工单 | 受理或拒绝 |
|
|
||||||
| `rejected` | 已拒绝 | 工单负责人拒绝 | 终态 |
|
|
||||||
| `processing` | 处理中 | 工单负责人受理 | 手动关闭或自动关闭 |
|
|
||||||
| `closed` | 已关闭 | 手动关闭或派生需求全部完成后自动关闭 | 终态 |
|
|
||||||
|
|
||||||
终态包括 `rejected`、`closed`。
|
|
||||||
|
|
||||||
### 6.3 自动关闭规则
|
|
||||||
|
|
||||||
1. 普通工单 / 子工单存在派生需求时,只有全部派生需求完成后才自动关闭。
|
|
||||||
2. 普通工单 / 子工单不存在派生需求时,允许工单负责人手动关闭,必须填写 `closeResult`。
|
|
||||||
3. 父工单至少存在一个子工单,且所有子工单均为 `rejected` 或 `closed` 后,自动关闭。
|
|
||||||
4. 父工单不直接检查需求完成情况,只汇总子工单终态。
|
|
||||||
|
|
||||||
## 7. 数据模型
|
|
||||||
|
|
||||||
### 7.1 工单主表
|
|
||||||
|
|
||||||
建议表名:`rdms_ticket`
|
|
||||||
|
|
||||||
| 字段 | 类型建议 | 必填规则 | 说明 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `id` | `bigint` | 是 | 主键 |
|
|
||||||
| `parent_id` | `bigint` | 否 | 父工单 ID;`child` 必填 |
|
|
||||||
| `ticket_mode` | `varchar(32)` | 是 | `single` / `parent` / `child` |
|
|
||||||
| `title` | `varchar(255)` | 是 | 工单标题 |
|
|
||||||
| `description` | `text` | 否 | 工单描述,支持富文本 |
|
|
||||||
| `ticket_type` | `varchar(32)` | 是 | 工单类型,字典 |
|
|
||||||
| `priority` | `varchar(32)` | 否 | 优先级,建议复用需求优先级字典 |
|
|
||||||
| `status_code` | `varchar(32)` | 是 | 工单状态 |
|
|
||||||
| `submitter_id` | `bigint` | 是 | 提交人用户 ID |
|
|
||||||
| `submitter_nickname` | `varchar(64)` | 否 | 提交人昵称快照 |
|
|
||||||
| `owner_id` | `bigint` | `single` / `child` 必填 | 工单负责人用户 ID |
|
|
||||||
| `owner_nickname` | `varchar(64)` | 否 | 工单负责人昵称快照 |
|
|
||||||
| `belong_type` | `varchar(32)` | `single` / `child` 必填 | `product` / `project` |
|
|
||||||
| `product_id` | `bigint` | 产品工单必填 | 归属产品 ID |
|
|
||||||
| `project_id` | `bigint` | 项目工单必填 | 归属项目 ID |
|
|
||||||
| `accept_time` | `datetime` | 否 | 受理时间 |
|
|
||||||
| `reject_reason` | `varchar(500)` | 拒绝时必填 | 拒绝原因 |
|
|
||||||
| `close_time` | `datetime` | 否 | 关闭时间 |
|
|
||||||
| `close_result` | `varchar(1000)` | 手动关闭时必填 | 处理结论 |
|
|
||||||
| `attachments` | `json` | 否 | 附件列表,沿用 `AttachmentItem` |
|
|
||||||
|
|
||||||
审计字段、逻辑删除字段复用现有 `BaseDO` 风格。
|
|
||||||
|
|
||||||
### 7.2 工单需求关联表
|
|
||||||
|
|
||||||
建议表名:`rdms_ticket_requirement_link`
|
|
||||||
|
|
||||||
| 字段 | 类型建议 | 必填规则 | 说明 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `id` | `bigint` | 是 | 主键 |
|
|
||||||
| `ticket_id` | `bigint` | 是 | 工单 ID |
|
|
||||||
| `target_type` | `varchar(32)` | 是 | `product_requirement` / `project_requirement` |
|
|
||||||
| `target_id` | `bigint` | 是 | 需求 ID |
|
|
||||||
|
|
||||||
不设置 `relationType`。本期关联表只表达“该工单派生了哪些需求”。
|
|
||||||
|
|
||||||
建议唯一约束:`ticket_id + target_type + target_id`。
|
|
||||||
|
|
||||||
## 8. 需求派生规则
|
|
||||||
|
|
||||||
### 8.1 产品工单派生产品需求
|
|
||||||
|
|
||||||
适用条件:
|
|
||||||
|
|
||||||
1. `ticketMode` 为 `single` 或 `child`。
|
|
||||||
2. `belongType = product`。
|
|
||||||
3. 工单状态为 `processing`。
|
|
||||||
|
|
||||||
派生结果:
|
|
||||||
|
|
||||||
1. 创建 `ProductRequirementDO`。
|
|
||||||
2. `productId` 使用工单 `productId`。
|
|
||||||
3. `sourceType = "work_order"`。
|
|
||||||
4. `sourceBizId = ticketId`。
|
|
||||||
5. 标题、描述、优先级、附件、提出人等字段可从工单带入,并允许派生表单二次编辑。
|
|
||||||
6. 写入 `rdms_ticket_requirement_link`,`targetType = product_requirement`。
|
|
||||||
|
|
||||||
后续链路沿用现有产品需求分发到项目的能力。
|
|
||||||
|
|
||||||
### 8.2 项目工单派生项目需求
|
|
||||||
|
|
||||||
适用条件:
|
|
||||||
|
|
||||||
1. `ticketMode` 为 `single` 或 `child`。
|
|
||||||
2. `belongType = project`。
|
|
||||||
3. 工单状态为 `processing`。
|
|
||||||
|
|
||||||
派生结果:
|
|
||||||
|
|
||||||
1. 创建 `ProjectRequirementDO`。
|
|
||||||
2. `projectId` 使用工单 `projectId`。
|
|
||||||
3. `sourceType = "work_order"`。
|
|
||||||
4. `sourceBizId = ticketId`。
|
|
||||||
5. 标题、描述、优先级、附件、提出人等字段可从工单带入,并允许派生表单二次编辑。
|
|
||||||
6. 写入 `rdms_ticket_requirement_link`,`targetType = project_requirement`。
|
|
||||||
|
|
||||||
后续执行和任务沿用现有项目需求、执行、任务链路。
|
|
||||||
|
|
||||||
### 8.3 完成判定
|
|
||||||
|
|
||||||
1. 产品工单只检查由该工单派生的产品需求。
|
|
||||||
2. 项目工单只检查由该工单派生的项目需求。
|
|
||||||
3. 需求完成态应复用现有需求状态模型的终态配置,不在工单逻辑中写死具体状态码。
|
|
||||||
4. 派生需求数量大于 0 且全部完成时,系统自动关闭对应工单。
|
|
||||||
|
|
||||||
## 9. 页面与待办
|
|
||||||
|
|
||||||
### 9.1 我的提交
|
|
||||||
|
|
||||||
展示当前用户提交的工单,包括:
|
|
||||||
|
|
||||||
1. 父工单。
|
|
||||||
2. 普通工单。
|
|
||||||
3. 子工单。
|
|
||||||
|
|
||||||
建议支持按状态、工单类型、归属类型、归属对象、创建时间筛选。
|
|
||||||
|
|
||||||
### 9.2 我的待处理
|
|
||||||
|
|
||||||
展示当前用户负责的普通工单和子工单,不展示父工单。
|
|
||||||
|
|
||||||
筛选条件:
|
|
||||||
|
|
||||||
1. `ownerId = 当前用户`。
|
|
||||||
2. `ticketMode in (single, child)`。
|
|
||||||
3. `statusCode in (pending_accept, processing)`。
|
|
||||||
|
|
||||||
### 9.3 父工单列表操作
|
|
||||||
|
|
||||||
工单列表查询中,父工单行需要在操作列展示“拆分子工单”入口。该入口只对父工单提交人或具备工单管理权限的用户可见。
|
|
||||||
|
|
||||||
点击后进入新增子工单表单,表单需要携带父工单上下文,并要求录入人员填写子工单的归属类型、归属产品/项目、工单负责人、工单类型等处理字段。
|
|
||||||
|
|
||||||
### 9.4 父工单汇总展示
|
|
||||||
|
|
||||||
父工单汇总展示需要包含:
|
|
||||||
|
|
||||||
1. 原始诉求信息。
|
|
||||||
2. 子工单列表。
|
|
||||||
3. 子工单归属产品/项目。
|
|
||||||
4. 子工单负责人。
|
|
||||||
5. 子工单状态。
|
|
||||||
6. 子工单派生需求数量与完成数量。
|
|
||||||
7. 父工单自动关闭结果。
|
|
||||||
|
|
||||||
## 10. 权限规则
|
|
||||||
|
|
||||||
1. 创建工单走全域权限。
|
|
||||||
2. 父工单新增子工单:父工单提交人或具备工单管理权限的用户可操作。
|
|
||||||
3. 普通工单 / 子工单受理、拒绝、手动关闭:工单负责人或具备工单管理权限的用户可操作。
|
|
||||||
4. 派生产品需求时,需要满足产品对象权限。
|
|
||||||
5. 派生项目需求时,需要满足项目对象权限。
|
|
||||||
6. 父工单不能执行受理、拒绝、派生需求、手动关闭动作。
|
|
||||||
|
|
||||||
## 11. 接口建议
|
|
||||||
|
|
||||||
接口路径建议落在 `rdms-project` 模块:
|
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| `POST` | `/project/tickets` | 创建普通工单或父工单 |
|
|
||||||
| `POST` | `/project/tickets/{parentId}/children` | 父工单新增子工单 |
|
|
||||||
| `GET` | `/project/tickets/my-submitted/page` | 我的提交 |
|
|
||||||
| `GET` | `/project/tickets/my-pending/page` | 我的待处理 |
|
|
||||||
| `GET` | `/project/tickets/{id}` | 工单详情 |
|
|
||||||
| `POST` | `/project/tickets/{id}/accept` | 受理 |
|
|
||||||
| `POST` | `/project/tickets/{id}/reject` | 拒绝 |
|
|
||||||
| `POST` | `/project/tickets/{id}/close` | 无派生需求时手动关闭 |
|
|
||||||
| `POST` | `/project/tickets/{id}/derive-product-requirement` | 派生产品需求 |
|
|
||||||
| `POST` | `/project/tickets/{id}/derive-project-requirement` | 派生项目需求 |
|
|
||||||
|
|
||||||
创建、更新类接口需要继续遵守仓库 HTTP 动词语义约定。部分动作使用语义化 `POST` 子动作接口,不引入 `PATCH`。
|
|
||||||
|
|
||||||
## 12. 校验规则
|
|
||||||
|
|
||||||
1. `ticketMode = parent` 时,`ownerId`、`belongType`、`productId`、`projectId` 必须为空。
|
|
||||||
2. `ticketMode = single` 时,`ownerId`、`belongType` 必填,且 `productId` / `projectId` 按归属类型二选一。
|
|
||||||
3. `ticketMode = child` 时,`parentId`、`ownerId`、`belongType` 必填,且父工单必须存在且 `ticketMode = parent`。
|
|
||||||
4. 产品工单必须填写 `productId`,不能填写 `projectId`。
|
|
||||||
5. 项目工单必须填写 `projectId`,不能填写 `productId`。
|
|
||||||
6. 只有 `pending_accept` 状态的普通工单 / 子工单可以受理或拒绝。
|
|
||||||
7. 只有 `processing` 状态的普通工单 / 子工单可以派生需求。
|
|
||||||
8. 有派生需求的工单不能手动关闭。
|
|
||||||
9. 无派生需求的 `processing` 工单可以手动关闭,必须填写 `closeResult`。
|
|
||||||
10. 父工单不能手动关闭。
|
|
||||||
11. 父工单至少有一个子工单,且全部子工单终态后才能自动关闭。
|
|
||||||
|
|
||||||
## 13. 错误处理
|
|
||||||
|
|
||||||
建议新增明确错误码覆盖以下场景:
|
|
||||||
|
|
||||||
1. 工单不存在。
|
|
||||||
2. 工单形态非法。
|
|
||||||
3. 父工单不能处理。
|
|
||||||
4. 子工单父级非法。
|
|
||||||
5. 工单归属类型非法。
|
|
||||||
6. 产品工单不能派生项目需求。
|
|
||||||
7. 项目工单不能派生产品需求。
|
|
||||||
8. 工单状态不允许当前动作。
|
|
||||||
9. 有派生需求的工单不能手动关闭。
|
|
||||||
10. 派生需求未全部完成,工单不能自动关闭。
|
|
||||||
|
|
||||||
## 14. 测试重点
|
|
||||||
|
|
||||||
1. 创建父工单后状态为 `splitting`,且不能受理、拒绝、派生需求、手动关闭。
|
|
||||||
2. 父工单可以持续新增多个子工单。
|
|
||||||
3. 子工单必须单归属产品或项目。
|
|
||||||
4. 普通工单 / 子工单创建后进入 `pending_accept`。
|
|
||||||
5. 工单负责人拒绝后进入 `rejected`。
|
|
||||||
6. 工单负责人受理后进入 `processing`。
|
|
||||||
7. 无派生需求的 `processing` 工单可手动关闭。
|
|
||||||
8. 有派生需求的工单不能手动关闭。
|
|
||||||
9. 产品工单只能派生产品需求。
|
|
||||||
10. 项目工单只能派生项目需求。
|
|
||||||
11. 派生需求全部完成后自动关闭工单。
|
|
||||||
12. 所有子工单终态后自动关闭父工单。
|
|
||||||
|
|
||||||
## 15. 风险与约束
|
|
||||||
|
|
||||||
1. 自动关闭依赖需求完成态判定,实施时必须和现有需求状态模型对齐。
|
|
||||||
2. 产品需求分发到项目后的项目需求、执行、任务链路不属于工单直接职责,工单只追踪自己直接派生的需求。
|
|
||||||
3. 父工单允许持续补子工单,会带来“父工单已关闭后是否允许继续补子单”的边界。本规格默认父工单关闭后不再补子单;如需重开父工单,需要单独设计重开动作。
|
|
||||||
4. 现有前端已有 `/ticket/my-submitted`、`/ticket/my-pending` 资源入口,后端接口落地时需要与前端路由和菜单权限同步。
|
|
||||||
|
|
||||||
@@ -19,7 +19,4 @@ public class DictDataRespDTO {
|
|||||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
private Integer status; // 参见 CommonStatusEnum 枚举
|
private Integer status; // 参见 CommonStatusEnum 枚举
|
||||||
|
|
||||||
@Schema(description = "标识", example = "system")
|
|
||||||
private String sign;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
|||||||
import com.njcn.rdms.gateway.util.SecurityFrameworkUtils;
|
import com.njcn.rdms.gateway.util.SecurityFrameworkUtils;
|
||||||
import com.njcn.rdms.gateway.util.WebFrameworkUtils;
|
import com.njcn.rdms.gateway.util.WebFrameworkUtils;
|
||||||
import com.njcn.rdms.module.system.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.system.enums.ErrorCodeConstants;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
|
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
@@ -30,6 +31,7 @@ import java.util.function.Function;
|
|||||||
|
|
||||||
import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
|
import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||||
|
|
||||||
@@ -57,8 +59,16 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginUser load(String token) {
|
public LoginUser load(String token) {
|
||||||
String body = checkAccessToken(token).block();
|
// 仅异步 refresh 走这里(同步链路用 getIfPresent + 直接 checkAccessToken,不触发 load)
|
||||||
return buildUser(body, token);
|
// 远端 token 已过期/校验失败时吞掉 ServiceException:
|
||||||
|
// 若抛出,会被 Guava 包成 ExecutionException 并由刷新线程池作为 UncaughtException 打到日志,看起来像故障。
|
||||||
|
try {
|
||||||
|
String body = checkAccessToken(token).block();
|
||||||
|
return buildUser(body, token);
|
||||||
|
} catch (ServiceException ex) {
|
||||||
|
log.info("[loginUserCache] 异步刷新忽略 token 校验失败:code={}, msg={}", ex.getCode(), ex.getMessage());
|
||||||
|
return LOGIN_USER_EMPTY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PRODUCT_CODE_DUPLICATE = new ErrorCode(1_008_001_001, "已经存在编码为【{}】的产品");
|
ErrorCode PRODUCT_CODE_DUPLICATE = new ErrorCode(1_008_001_001, "已经存在编码为【{}】的产品");
|
||||||
ErrorCode PRODUCT_NAME_DUPLICATE = new ErrorCode(1_008_001_002, "已经存在名称为【{}】的产品");
|
ErrorCode PRODUCT_NAME_DUPLICATE = new ErrorCode(1_008_001_002, "已经存在名称为【{}】的产品");
|
||||||
ErrorCode PRODUCT_CODE_NOT_MODIFIABLE = new ErrorCode(1_008_001_003, "产品编码创建后不允许修改");
|
ErrorCode PRODUCT_CODE_NOT_MODIFIABLE = new ErrorCode(1_008_001_003, "产品编码创建后不允许修改");
|
||||||
ErrorCode PRODUCT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_001_004, "当前产品状态不支持动作【{}】");
|
ErrorCode PRODUCT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_001_004, "当前产品为「{}」状态,不支持「{}」操作");
|
||||||
ErrorCode PRODUCT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_001_005, "动作【{}】必须填写原因");
|
ErrorCode PRODUCT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_001_005, "「{}」操作必须填写原因");
|
||||||
ErrorCode PRODUCT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_001_006, "删除确认名称与当前产品名称不一致");
|
ErrorCode PRODUCT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_001_006, "删除确认名称与当前产品名称不一致");
|
||||||
ErrorCode PRODUCT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_001_007, "当前产品状态不允许编辑");
|
ErrorCode PRODUCT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_001_007, "当前产品状态不允许编辑");
|
||||||
ErrorCode PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE = new ErrorCode(1_008_001_008, "产品暂停后仅允许修正描述,产品经理请通过产品团队维护");
|
ErrorCode PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE = new ErrorCode(1_008_001_008, "产品暂停后仅允许修正描述,产品经理请通过产品团队维护");
|
||||||
@@ -29,7 +29,7 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PRODUCT_MANAGER_TRANSFER_SOURCE_INVALID = new ErrorCode(1_008_001_017, "原产品经理信息与当前产品经理不一致");
|
ErrorCode PRODUCT_MANAGER_TRANSFER_SOURCE_INVALID = new ErrorCode(1_008_001_017, "原产品经理信息与当前产品经理不一致");
|
||||||
ErrorCode PRODUCT_MANAGER_TRANSFER_ROLE_INVALID = new ErrorCode(1_008_001_018, "原产品经理交接后的角色不能仍为产品经理");
|
ErrorCode PRODUCT_MANAGER_TRANSFER_ROLE_INVALID = new ErrorCode(1_008_001_018, "原产品经理交接后的角色不能仍为产品经理");
|
||||||
ErrorCode PRODUCT_MANAGER_NOT_MODIFIABLE = new ErrorCode(1_008_001_019, "产品主数据编辑不允许直接变更产品经理,请通过产品团队维护");
|
ErrorCode PRODUCT_MANAGER_NOT_MODIFIABLE = new ErrorCode(1_008_001_019, "产品主数据编辑不允许直接变更产品经理,请通过产品团队维护");
|
||||||
ErrorCode PRODUCT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_001_020, "当前用户不具备该产品的操作权限【{}】");
|
ErrorCode PRODUCT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_001_020, "您没有该产品的此项操作权限,请联系管理员");
|
||||||
ErrorCode PRODUCT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_001_021, "删除确认口令不正确");
|
ErrorCode PRODUCT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_001_021, "删除确认口令不正确");
|
||||||
ErrorCode PRODUCT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_001_022, "产品状态已发生变化,请刷新后重试");
|
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, "产品状态定义不存在或已停用");
|
ErrorCode PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_001_023, "产品状态定义不存在或已停用");
|
||||||
@@ -38,7 +38,7 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PRODUCT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_001_025, "初始团队成员存在重复");
|
ErrorCode PRODUCT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_001_025, "初始团队成员存在重复");
|
||||||
ErrorCode PRODUCT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_001_026, "初始团队中存在非法角色");
|
ErrorCode PRODUCT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_001_026, "初始团队中存在非法角色");
|
||||||
ErrorCode PRODUCT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_001_027, "原产品经理在该产品已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试");
|
ErrorCode PRODUCT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_001_027, "原产品经理在该产品已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试");
|
||||||
ErrorCode PRODUCT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_001_028, "内置产品角色【{}】未在 system_role 找到,请联系管理员");
|
ErrorCode PRODUCT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_001_028, "内置产品角色【{}】未配置,请联系管理员");
|
||||||
ErrorCode PRODUCT_MEMBER_USER_INVALID = new ErrorCode(1_008_001_029, "产品成员不是有效系统用户");
|
ErrorCode PRODUCT_MEMBER_USER_INVALID = new ErrorCode(1_008_001_029, "产品成员不是有效系统用户");
|
||||||
// 批量新增(POST /project/product/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
// 批量新增(POST /project/product/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
||||||
ErrorCode PRODUCT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_001_030, "请勿在批量列表中重复添加同一成员");
|
ErrorCode PRODUCT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_001_030, "请勿在批量列表中重复添加同一成员");
|
||||||
@@ -48,14 +48,14 @@ public interface ErrorCodeConstants {
|
|||||||
|
|
||||||
// ========== 产品需求 1-008-002-000 ==========
|
// ========== 产品需求 1-008-002-000 ==========
|
||||||
ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在");
|
ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在");
|
||||||
ErrorCode REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_001, "当前需求状态不支持动作【{}】");
|
ErrorCode REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_001, "当前需求为「{}」状态,不支持「{}」操作");
|
||||||
ErrorCode REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_002, "动作【{}】必须填写原因");
|
ErrorCode REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_002, "「{}」操作必须填写原因");
|
||||||
ErrorCode REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_003, "需求状态已发生变化,请刷新后重试");
|
ErrorCode REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_003, "需求状态已发生变化,请刷新后重试");
|
||||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_004, "当前需求状态为终态,不允许编辑");
|
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_004, "当前需求状态不允许编辑");
|
||||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_005, "只有已验收的需求才能关闭");
|
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_005, "只有已验收的需求才能关闭");
|
||||||
ErrorCode REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_006, "需求状态定义不存在或已停用");
|
ErrorCode REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_006, "需求状态定义不存在或已停用");
|
||||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待确认、待评审、待分流状态的需求才能删除");
|
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待认领、待评审、待指派状态的需求才能删除");
|
||||||
ErrorCode REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_007, "父需求状态不是待分流或实施中,不允许拆分");
|
ErrorCode REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_007, "父需求状态不是已评审、待指派、实施中,不允许拆分");
|
||||||
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_008, "存在未处理完的子需求,请先处理子需求");
|
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_008, "存在未处理完的子需求,请先处理子需求");
|
||||||
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_002_017, "只有不存在子需求,或子需求都处于已取消和已拒绝的状态才能取消");
|
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_002_017, "只有不存在子需求,或子需求都处于已取消和已拒绝的状态才能取消");
|
||||||
ErrorCode REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_002_013, "存在子需求,请先删除子需求");
|
ErrorCode REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_002_013, "存在子需求,请先删除子需求");
|
||||||
@@ -65,12 +65,15 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS = new ErrorCode(1_008_002_012, "模块下存在非终态需求,不可删除");
|
ErrorCode REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS = new ErrorCode(1_008_002_012, "模块下存在非终态需求,不可删除");
|
||||||
ErrorCode REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_002_015, "存在子模块,请先删除子模块");
|
ErrorCode REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_002_015, "存在子模块,请先删除子模块");
|
||||||
ErrorCode REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_002_016, "模块下存在需求,请先删除需求");
|
ErrorCode REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_002_016, "模块下存在需求,请先删除需求");
|
||||||
ErrorCode PROJECT_REQUIREMENT_MODULE_ROOT_NOT_EXISTS = new ErrorCode(1_008_002_018, "关联项目下不存在根模块,请先创建项目模块");
|
ErrorCode REQUIREMENT_PROJECT_MODULE_ROOT_NOT_EXISTS = new ErrorCode(1_008_002_018, "关联项目下不存在根模块,请先创建项目根模块");
|
||||||
ErrorCode REQUIREMENT_DISPATCHED_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_019, "产品需求已分流生成项目需求,不允许再在产品端拆分");
|
ErrorCode REQUIREMENT_DISPATCHED_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_019, "产品需求已指派生成项目需求,不允许再在产品端拆分");
|
||||||
ErrorCode REQUIREMENT_NOT_DISPATCHED = new ErrorCode(1_008_002_020, "该产品需求尚未分流到关联项目");
|
ErrorCode REQUIREMENT_NOT_DISPATCHED = new ErrorCode(1_008_002_020, "该产品需求尚未指派到关联项目");
|
||||||
ErrorCode REQUIREMENT_DISPATCHED_PROJECT_REQUIREMENT_NOT_FOUND = new ErrorCode(1_008_002_021, "未找到该产品需求对应的项目需求");
|
ErrorCode REQUIREMENT_DISPATCHED_PROJECT_REQUIREMENT_NOT_FOUND = new ErrorCode(1_008_002_021, "未找到该产品需求对应的项目需求");
|
||||||
|
ErrorCode REQUIREMENT_HANDLER_NOT_PRODUCT_MEMBER = new ErrorCode(1_008_002_023, "当前需求负责人不是此产品团队成员,请重新选择");
|
||||||
|
ErrorCode REQUIREMENT_REVIEW_ALREADY_EXISTS = new ErrorCode(1_008_002_024, "该产品需求已提交评审记录");
|
||||||
|
ErrorCode REQUIREMENT_REVIEW_NOT_EXISTS = new ErrorCode(1_008_002_025, "产品需求评审记录不存在");
|
||||||
|
ErrorCode REQUIREMENT_REVIEW_CONCLUSION_INVALID = new ErrorCode(1_008_002_026, "产品需求评审结论不合法");
|
||||||
ErrorCode REQUIREMENT_NOT_PROJECT_MEMBER = new ErrorCode(1_008_002_022, "您不是该项目的成员,无权访问");
|
ErrorCode REQUIREMENT_NOT_PROJECT_MEMBER = new ErrorCode(1_008_002_022, "您不是该项目的成员,无权访问");
|
||||||
ErrorCode REQUIREMENT_HANDLER_NOT_PROJECT_MEMBER = new ErrorCode(1_008_002_023, "当前需求负责人不是所选分流项目的成员,请重新选择");
|
|
||||||
|
|
||||||
// ========== 项目管理 1-008-002-000 ==========
|
// ========== 项目管理 1-008-002-000 ==========
|
||||||
ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1_008_002_000, "项目不存在");
|
ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1_008_002_000, "项目不存在");
|
||||||
@@ -82,8 +85,8 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PROJECT_TYPE_INVALID = new ErrorCode(1_008_002_006, "项目类型不是有效字典值");
|
ErrorCode PROJECT_TYPE_INVALID = new ErrorCode(1_008_002_006, "项目类型不是有效字典值");
|
||||||
ErrorCode PROJECT_MANAGER_USER_INVALID = new ErrorCode(1_008_002_007, "项目负责人不是有效系统用户");
|
ErrorCode PROJECT_MANAGER_USER_INVALID = new ErrorCode(1_008_002_007, "项目负责人不是有效系统用户");
|
||||||
ErrorCode PROJECT_MEMBER_USER_INVALID = new ErrorCode(1_008_002_008, "项目成员不是有效系统用户");
|
ErrorCode PROJECT_MEMBER_USER_INVALID = new ErrorCode(1_008_002_008, "项目成员不是有效系统用户");
|
||||||
ErrorCode PROJECT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_009, "当前项目状态不支持动作【{}】");
|
ErrorCode PROJECT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_009, "当前项目为「{}」状态,不支持「{}」操作");
|
||||||
ErrorCode PROJECT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_010, "动作【{}】必须填写原因");
|
ErrorCode PROJECT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_010, "「{}」操作必须填写原因");
|
||||||
ErrorCode PROJECT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_011, "项目状态已发生变化,请刷新后重试");
|
ErrorCode PROJECT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_011, "项目状态已发生变化,请刷新后重试");
|
||||||
ErrorCode PROJECT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_012, "当前项目状态不允许编辑");
|
ErrorCode PROJECT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_012, "当前项目状态不允许编辑");
|
||||||
ErrorCode PROJECT_MEMBER_NOT_EXISTS = new ErrorCode(1_008_002_013, "项目成员不存在");
|
ErrorCode PROJECT_MEMBER_NOT_EXISTS = new ErrorCode(1_008_002_013, "项目成员不存在");
|
||||||
@@ -94,7 +97,7 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PROJECT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_002_018, "删除确认口令不正确");
|
ErrorCode PROJECT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_002_018, "删除确认口令不正确");
|
||||||
ErrorCode PROJECT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_002_019, "删除确认名称与当前项目名称不一致");
|
ErrorCode PROJECT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_002_019, "删除确认名称与当前项目名称不一致");
|
||||||
ErrorCode PROJECT_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_020, "当前项目不允许删除");
|
ErrorCode PROJECT_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_020, "当前项目不允许删除");
|
||||||
ErrorCode PROJECT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_002_021, "当前用户不具备该项目的操作权限【{}】");
|
ErrorCode PROJECT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_002_021, "您没有该项目的此项操作权限,请联系管理员");
|
||||||
ErrorCode PROJECT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_022, "项目状态定义不存在或已停用");
|
ErrorCode PROJECT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_022, "项目状态定义不存在或已停用");
|
||||||
ErrorCode PROJECT_DIRECTION_INVALID = new ErrorCode(1_008_002_023, "项目方向不是有效字典值");
|
ErrorCode PROJECT_DIRECTION_INVALID = new ErrorCode(1_008_002_023, "项目方向不是有效字典值");
|
||||||
ErrorCode PROJECT_MANAGER_TRANSFER_INFO_REQUIRED = new ErrorCode(1_008_002_024, "切换项目经理时必须同时传入原项目经理用户和交接后角色");
|
ErrorCode PROJECT_MANAGER_TRANSFER_INFO_REQUIRED = new ErrorCode(1_008_002_024, "切换项目经理时必须同时传入原项目经理用户和交接后角色");
|
||||||
@@ -108,7 +111,7 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PROJECT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_002_031, "初始团队中存在非法角色");
|
ErrorCode PROJECT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_002_031, "初始团队中存在非法角色");
|
||||||
ErrorCode PROJECT_DIRECTION_NOT_MATCH_PRODUCT = new ErrorCode(1_008_002_032, "项目方向与所属产品方向不一致");
|
ErrorCode PROJECT_DIRECTION_NOT_MATCH_PRODUCT = new ErrorCode(1_008_002_032, "项目方向与所属产品方向不一致");
|
||||||
ErrorCode PROJECT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_002_033, "原项目经理在该项目已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试");
|
ErrorCode PROJECT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_002_033, "原项目经理在该项目已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试");
|
||||||
ErrorCode PROJECT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_002_034, "内置项目角色【{}】未在 system_role 找到,请联系管理员");
|
ErrorCode PROJECT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_002_034, "内置项目角色【{}】未配置,请联系管理员");
|
||||||
// 批量新增(POST /project/project/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
// 批量新增(POST /project/project/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
||||||
ErrorCode PROJECT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_002_035, "请勿在批量列表中重复添加同一成员");
|
ErrorCode PROJECT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_002_035, "请勿在批量列表中重复添加同一成员");
|
||||||
ErrorCode PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_002_036, "批量新增不允许指定为经理,请通过编辑成员调整");
|
ErrorCode PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_002_036, "批量新增不允许指定为经理,请通过编辑成员调整");
|
||||||
@@ -128,13 +131,13 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_008, "当前项目状态不允许维护执行");
|
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_008, "当前项目状态不允许维护执行");
|
||||||
ErrorCode PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED = new ErrorCode(1_008_003_009, "该项目成员仍担任未终态执行负责人,请先完成执行负责人交接");
|
ErrorCode PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED = new ErrorCode(1_008_003_009, "该项目成员仍担任未终态执行负责人,请先完成执行负责人交接");
|
||||||
ErrorCode PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_003_010, "执行状态定义不存在或已停用");
|
ErrorCode PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_003_010, "执行状态定义不存在或已停用");
|
||||||
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_003_011, "当前执行状态不支持动作【{}】");
|
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_003_011, "当前执行为「{}」状态,不支持「{}」操作");
|
||||||
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_003_012, "动作【{}】必须填写原因");
|
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_003_012, "「{}」操作必须填写原因");
|
||||||
ErrorCode PROJECT_EXECUTION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_003_013, "执行状态已发生变化,请刷新后重试");
|
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_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_014, "当前执行状态不允许维护执行");
|
||||||
ErrorCode PROJECT_EXECUTION_TYPE_INVALID = new ErrorCode(1_008_003_015, "执行类型不是有效字典值");
|
ErrorCode PROJECT_EXECUTION_TYPE_INVALID = new ErrorCode(1_008_003_015, "执行类型不是有效字典值");
|
||||||
ErrorCode PROJECT_EXECUTION_ASSIGNEE_REQUIRED = new ErrorCode(1_008_003_016, "创建执行时必须至少选择一名执行协办人");
|
ErrorCode PROJECT_EXECUTION_ASSIGNEE_REQUIRED = new ErrorCode(1_008_003_016, "创建执行时必须至少选择一名执行协办人");
|
||||||
ErrorCode PROJECT_EXECUTION_STATUS_OWNER_ONLY = new ErrorCode(1_008_003_017, "只有执行负责人才能执行【{}】动作");
|
ErrorCode PROJECT_EXECUTION_STATUS_OWNER_ONLY = new ErrorCode(1_008_003_017, "只有执行负责人才能执行「{}」操作");
|
||||||
ErrorCode PROJECT_EXECUTION_COMPLETE_TASKS_REQUIRED = new ErrorCode(1_008_003_018, "完成执行前,执行下所有任务必须全部完成或取消");
|
ErrorCode PROJECT_EXECUTION_COMPLETE_TASKS_REQUIRED = new ErrorCode(1_008_003_018, "完成执行前,执行下所有任务必须全部完成或取消");
|
||||||
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_DELETE = new ErrorCode(1_008_003_019, "已完成的执行不允许删除");
|
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_NAME_MISMATCH = new ErrorCode(1_008_003_020, "确认执行名称与实际不一致");
|
||||||
@@ -150,12 +153,12 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PROJECT_TASK_PARENT_INVALID = new ErrorCode(1_008_004_002, "父任务必须属于当前项目和执行");
|
ErrorCode PROJECT_TASK_PARENT_INVALID = new ErrorCode(1_008_004_002, "父任务必须属于当前项目和执行");
|
||||||
ErrorCode PROJECT_TASK_NOT_ALLOW_EDIT = new ErrorCode(1_008_004_003, "当前项目或执行状态不允许维护任务");
|
ErrorCode PROJECT_TASK_NOT_ALLOW_EDIT = new ErrorCode(1_008_004_003, "当前项目或执行状态不允许维护任务");
|
||||||
ErrorCode PROJECT_TASK_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_004_004, "任务状态定义不存在或已停用");
|
ErrorCode PROJECT_TASK_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_004_004, "任务状态定义不存在或已停用");
|
||||||
ErrorCode PROJECT_TASK_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_004_005, "当前任务状态不支持动作【{}】");
|
ErrorCode PROJECT_TASK_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_004_005, "当前任务为「{}」状态,不支持「{}」操作");
|
||||||
ErrorCode PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_004_006, "动作【{}】必须填写原因");
|
ErrorCode PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_004_006, "「{}」操作必须填写原因");
|
||||||
ErrorCode PROJECT_TASK_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_004_007, "任务状态已发生变化,请刷新后重试");
|
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_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_COMPLETE_CHILDREN_REQUIRED = new ErrorCode(1_008_004_010, "父任务完成前,子任务必须全部完成或取消");
|
||||||
ErrorCode PROJECT_TASK_STATUS_OWNER_ONLY = new ErrorCode(1_008_004_011, "只有任务负责人才能执行【{}】动作");
|
ErrorCode PROJECT_TASK_STATUS_OWNER_ONLY = new ErrorCode(1_008_004_011, "只有任务负责人才能执行「{}」操作");
|
||||||
ErrorCode PROJECT_TASK_NOT_ALLOW_DELETE = new ErrorCode(1_008_004_012, "已完成的任务不允许删除");
|
ErrorCode PROJECT_TASK_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_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_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_004_014, "删除确认口令必须为 DELETE 或 删除");
|
||||||
@@ -192,33 +195,49 @@ public interface ErrorCodeConstants {
|
|||||||
|
|
||||||
// ========== 项目需求 1_008_007_xxx ==========
|
// ========== 项目需求 1_008_007_xxx ==========
|
||||||
ErrorCode PROJECT_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "项目需求不存在");
|
ErrorCode PROJECT_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "项目需求不存在");
|
||||||
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_007_001, "当前项目需求状态不支持动作【{}】");
|
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_007_001, "当前项目需求为「{}」状态,不支持「{}」操作");
|
||||||
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_007_002, "动作【{}】必须填写原因");
|
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_007_002, "「{}」操作必须填写原因");
|
||||||
ErrorCode PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_007_003, "项目需求状态已发生变化,请刷新后重试");
|
ErrorCode PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_007_003, "项目需求状态已发生变化,请刷新后重试");
|
||||||
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_007_004, "当前项目需求状态为终态,不允许编辑");
|
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_007_004, "当前项目需求状态不允许编辑");
|
||||||
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_005, "只有已验收的项目需求才能关闭");
|
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_005, "只有已验收的项目需求才能关闭");
|
||||||
ErrorCode PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_007_006, "项目需求状态定义不存在或已停用");
|
ErrorCode PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_007_006, "项目需求状态定义不存在或已停用");
|
||||||
ErrorCode PROJECT_REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_007_007, "父需求状态不是实施中,不允许拆分");
|
ErrorCode PROJECT_REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_007_007, "父需求状态不是已评审、实施中,不允许拆分");
|
||||||
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_008, "存在未处理完的子需求,请先处理子需求");
|
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_008, "存在未处理完的子需求,请先处理子需求");
|
||||||
ErrorCode PROJECT_REQUIREMENT_MODULE_NOT_EXISTS = new ErrorCode(1_008_007_009, "项目需求模块不存在");
|
ErrorCode PROJECT_REQUIREMENT_MODULE_NOT_EXISTS = new ErrorCode(1_008_007_009, "项目需求模块不存在");
|
||||||
ErrorCode PROJECT_REQUIREMENT_MODULE_NAME_DUPLICATE = new ErrorCode(1_008_007_010, "已经存在名称为【{}】的项目需求模块");
|
ErrorCode PROJECT_REQUIREMENT_MODULE_NAME_DUPLICATE = new ErrorCode(1_008_007_010, "已经存在名称为【{}】的项目需求模块");
|
||||||
ErrorCode PROJECT_REQUIREMENT_MODULE_NOT_BELONG_TO_PROJECT = new ErrorCode(1_008_007_011, "模块不属于当前项目");
|
ErrorCode PROJECT_REQUIREMENT_MODULE_NOT_BELONG_TO_PROJECT = new ErrorCode(1_008_007_011, "模块不属于当前项目");
|
||||||
ErrorCode PROJECT_REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_007_013, "存在子需求,请先删除子需求");
|
ErrorCode PROJECT_REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_007_013, "存在子需求,请先删除子需求");
|
||||||
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_007_014, "只有待确认、待评审状态的项目需求才能删除");
|
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_007_014, "只有待认领、待评审状态的项目需求才能删除");
|
||||||
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_007_015, "存在子模块,请先删除子模块");
|
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_007_015, "存在子模块,请先删除子模块");
|
||||||
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_007_016, "模块下存在项目需求,请先删除需求");
|
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_007_016, "模块下存在项目需求,请先删除需求");
|
||||||
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_017, "只有不存在子需求,或子需求都处于已取消和已拒绝状态时,父需求才允许取消");
|
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_017, "只有不存在子需求,或子需求都处于已取消和已拒绝状态时,父需求才允许取消");
|
||||||
ErrorCode PROJECT_REQUIREMENT_HAS_EXECUTIONS_NOT_ALLOW_DELETE = new ErrorCode(1_008_007_018, "该项目需求下存在承接执行,请先解绑或转移");
|
ErrorCode PROJECT_REQUIREMENT_HAS_EXECUTIONS_NOT_ALLOW_DELETE = new ErrorCode(1_008_007_018, "该项目需求下存在承接执行,请先解绑或转移");
|
||||||
ErrorCode PROJECT_REQUIREMENT_SYNCED_FROM_PRODUCT_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_019, "\u7531\u4ea7\u54c1\u9700\u6c42\u6d41\u8f6c\u751f\u6210\u7684\u9879\u76ee\u9700\u6c42\u4e0d\u5141\u8bb8\u53d6\u6d88");
|
ErrorCode PROJECT_REQUIREMENT_SYNCED_FROM_PRODUCT_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_019, "从产品侧流转来的需求不可取消");
|
||||||
|
ErrorCode PROJECT_REQUIREMENT_REVIEW_ALREADY_EXISTS = new ErrorCode(1_008_007_020, "该项目需求已提交评审记录");
|
||||||
|
ErrorCode PROJECT_REQUIREMENT_REVIEW_NOT_EXISTS = new ErrorCode(1_008_007_021, "项目需求评审记录不存在");
|
||||||
|
ErrorCode PROJECT_REQUIREMENT_REVIEW_CONCLUSION_INVALID = new ErrorCode(1_008_007_022, "项目需求评审结论不合法");
|
||||||
|
|
||||||
// ========== 个人事项 1_008_008_xxx ==========
|
// ========== 个人事项 1_008_008_xxx ==========
|
||||||
ErrorCode PERSONAL_ITEM_NOT_EXISTS = new ErrorCode(1_008_008_001, "个人事项不存在");
|
ErrorCode PERSONAL_ITEM_NOT_EXISTS = new ErrorCode(1_008_008_001, "个人事项不存在");
|
||||||
ErrorCode PERSONAL_ITEM_OWNER_NOT_IN_EXECUTION = new ErrorCode(1_008_008_002, "个人事项负责人必须属于当前有效执行团队成员");
|
ErrorCode PERSONAL_ITEM_OWNER_NOT_IN_EXECUTION = new ErrorCode(1_008_008_002, "个人事项负责人必须属于当前有效执行团队成员");
|
||||||
ErrorCode PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_008_003, "个人事项状态定义不存在或已停用");
|
ErrorCode PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_008_003, "个人事项状态定义不存在或已停用");
|
||||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_008_004, "当前个人事项状态不支持动作【{}】");
|
ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_008_004, "当前个人事项为「{}」状态,不支持「{}」操作");
|
||||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_008_005, "动作【{}】必须填写原因");
|
ErrorCode PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_008_005, "「{}」操作必须填写原因");
|
||||||
ErrorCode PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_008_006, "个人事项状态已发生变化,请刷新后重试");
|
ErrorCode PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_008_006, "个人事项状态已发生变化,请刷新后重试");
|
||||||
ErrorCode PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_008_007, "当前个人事项状态不允许编辑");
|
ErrorCode PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_008_007, "当前个人事项状态不允许编辑");
|
||||||
ErrorCode PERSONAL_ITEM_NOT_ALLOW_DELETE = new ErrorCode(1_008_008_008, "仅初始态(待开始)的个人事项允许删除");
|
ErrorCode PERSONAL_ITEM_NOT_ALLOW_DELETE = new ErrorCode(1_008_008_008, "仅初始态(待开始)的个人事项允许删除");
|
||||||
ErrorCode PERSONAL_ITEM_WRITE_FORBIDDEN = new ErrorCode(1_008_008_009, "无权修改个人事项");
|
ErrorCode PERSONAL_ITEM_WRITE_FORBIDDEN = new ErrorCode(1_008_008_009, "无权修改个人事项");
|
||||||
|
|
||||||
|
// ========== 加班申请 1_008_009_xxx ==========
|
||||||
|
ErrorCode OVERTIME_APPLICATION_NOT_EXISTS = new ErrorCode(1_008_009_001, "加班申请不存在");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_009_002, "加班申请状态定义不存在或已停用");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_009_003, "当前加班申请为「{}」状态,不支持「{}」操作");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_009_004, "「{}」操作必须填写原因");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_009_005, "加班申请状态已发生变化,请刷新后重试");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_APPLICANT_ONLY = new ErrorCode(1_008_009_006, "仅申请人可执行该操作");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_APPROVER_ONLY = new ErrorCode(1_008_009_007, "仅当前审核人可执行该操作");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_APPROVER_INVALID = new ErrorCode(1_008_009_008, "审核人不是有效系统用户");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN = new ErrorCode(1_008_009_009, "审核人不能选择申请人本人");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_READ_FORBIDDEN = new ErrorCode(1_008_009_010, "无权查看该加班申请");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED = new ErrorCode(1_008_009_011, "仅已撤销的加班申请允许删除");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.njcn.rdms.module.project.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班申请常量。
|
||||||
|
*/
|
||||||
|
public final class OvertimeApplicationConstants {
|
||||||
|
|
||||||
|
private OvertimeApplicationConstants() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String BIZ_TYPE = "overtime_application";
|
||||||
|
public static final String STATUS_OBJECT_TYPE = BIZ_TYPE;
|
||||||
|
|
||||||
|
public static final String STATUS_PENDING = "pending";
|
||||||
|
public static final String STATUS_APPROVED = "approved";
|
||||||
|
public static final String STATUS_REJECTED = "rejected";
|
||||||
|
public static final String STATUS_CANCELLED = "cancelled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建即提交的业务日志动作。
|
||||||
|
* <p>
|
||||||
|
* 该动作仅用于状态日志、审计日志留痕,不要求在 rdms_object_status_transition 中存在显式流转配置。
|
||||||
|
*/
|
||||||
|
public static final String ACTION_SUBMIT = "submit";
|
||||||
|
public static final String ACTION_RESUBMIT = "resubmit";
|
||||||
|
public static final String ACTION_APPROVE = "approve";
|
||||||
|
public static final String ACTION_REJECT = "reject";
|
||||||
|
public static final String ACTION_CANCEL = "cancel";
|
||||||
|
public static final String ACTION_DELETE = "delete";
|
||||||
|
|
||||||
|
public static final String PERMISSION_QUERY = "project:overtime-application:query";
|
||||||
|
public static final String PERMISSION_CREATE = "project:overtime-application:create";
|
||||||
|
public static final String PERMISSION_UPDATE = "project:overtime-application:update";
|
||||||
|
public static final String PERMISSION_DELETE = "project:overtime-application:delete";
|
||||||
|
public static final String PERMISSION_APPROVE = "project:overtime-application:approve";
|
||||||
|
public static final String PERMISSION_EXPORT = "project:overtime-application:export";
|
||||||
|
|
||||||
|
}
|
||||||
@@ -38,6 +38,11 @@ public final class ProductObjectConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String PERMISSION_STATUS = "project:product:status";
|
public static final String PERMISSION_STATUS = "project:product:status";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品需求评审权限码。
|
||||||
|
*/
|
||||||
|
public static final String PERMISSION_REVIEW = "project:product:review";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 产品删除权限码。
|
* 产品删除权限码。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ public final class ProjectExecutionConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String BIZ_TYPE = "project_execution";
|
public static final String BIZ_TYPE = "project_execution";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行读路径查询权限码(对象域,object_type='project')。
|
||||||
|
* 覆盖执行对象所有读路径:page / status-board / detail。
|
||||||
|
* "我参与 / 所有"视角由前端发不发 involveUserId 决定;进得来 = 看项目下全部,无此权限码直接 403。
|
||||||
|
*/
|
||||||
|
public static final String PERMISSION_QUERY = "project:execution:query";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建执行权限码。
|
* 创建执行权限码。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ public final class ProjectObjectConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String PERMISSION_STATUS = "project:project:status";
|
public static final String PERMISSION_STATUS = "project:project:status";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目需求评审权限码。
|
||||||
|
*/
|
||||||
|
public static final String PERMISSION_REVIEW = "project:project:review";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目拆分权限码。
|
* 项目拆分权限码。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ public final class ProjectTaskConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String BIZ_TYPE = "project_task";
|
public static final String BIZ_TYPE = "project_task";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务读路径查询权限码(对象域,object_type='project')。
|
||||||
|
* 覆盖任务对象所有读路径:page / status-board / board-page / detail / summary(含跨执行 aggregate)。
|
||||||
|
* "我参与 / 所有"视角由前端发不发 involveUserId 决定;进得来 = 看项目下全部,无此权限码直接 403。
|
||||||
|
*/
|
||||||
|
public static final String PERMISSION_QUERY = "project:task:query";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建任务权限码。
|
* 创建任务权限码。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
|
||||||
|
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusDictRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
|
||||||
|
import com.njcn.rdms.module.project.service.overtime.OvertimeApplicationService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
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;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
|
||||||
|
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 加班申请")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/project/overtime-applications")
|
||||||
|
@Validated
|
||||||
|
public class OvertimeApplicationController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OvertimeApplicationService overtimeApplicationService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "新增加班申请并提交")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_CREATE + "')")
|
||||||
|
public CommonResult<Long> create(@Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) {
|
||||||
|
return success(overtimeApplicationService.createApplication(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "退回或撤销后修改并重新提交加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')")
|
||||||
|
public CommonResult<Boolean> resubmit(@PathVariable("id") Long id,
|
||||||
|
@Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) {
|
||||||
|
overtimeApplicationService.resubmitApplication(id, reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取加班申请详情")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
|
||||||
|
public CommonResult<OvertimeApplicationRespVO> get(@PathVariable("id") Long id) {
|
||||||
|
return success(overtimeApplicationService.getApplication(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/status/dict")
|
||||||
|
@Operation(summary = "获取加班申请所有状态字典")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
|
||||||
|
public CommonResult<List<OvertimeApplicationStatusDictRespVO>> getStatusDict() {
|
||||||
|
return success(overtimeApplicationService.getStatusDict());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "获取我的加班申请分页")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
|
||||||
|
public CommonResult<PageResult<OvertimeApplicationRespVO>> page(@Valid OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
return success(overtimeApplicationService.getMyPage(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/approval-page")
|
||||||
|
@Operation(summary = "获取待我审批的加班申请分页")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
|
||||||
|
public CommonResult<PageResult<OvertimeApplicationRespVO>> approvalPage(@Valid OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
return success(overtimeApplicationService.getApprovalPage(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/approve")
|
||||||
|
@Operation(summary = "审核通过加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
|
||||||
|
public CommonResult<Boolean> approve(@PathVariable("id") Long id,
|
||||||
|
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
overtimeApplicationService.approve(id, reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/reject")
|
||||||
|
@Operation(summary = "审核退回加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
|
||||||
|
public CommonResult<Boolean> reject(@PathVariable("id") Long id,
|
||||||
|
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
overtimeApplicationService.reject(id, reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/cancel")
|
||||||
|
@Operation(summary = "撤销加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')")
|
||||||
|
public CommonResult<Boolean> cancel(@PathVariable("id") Long id,
|
||||||
|
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
overtimeApplicationService.cancel(id, reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "删除已撤销的加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_DELETE + "')")
|
||||||
|
public CommonResult<Boolean> delete(@PathVariable("id") Long id) {
|
||||||
|
overtimeApplicationService.deleteApplication(id);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/status-logs")
|
||||||
|
@Operation(summary = "获取加班申请状态日志")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
|
||||||
|
public CommonResult<List<OvertimeApplicationStatusLogRespVO>> statusLogs(@PathVariable("id") Long id) {
|
||||||
|
return success(overtimeApplicationService.getStatusLogs(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/export")
|
||||||
|
@Operation(summary = "导出我的加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_EXPORT + "')")
|
||||||
|
@ApiAccessLog(operateType = EXPORT)
|
||||||
|
public void export(@Valid OvertimeApplicationPageReqVO reqVO, HttpServletResponse response) throws IOException {
|
||||||
|
List<OvertimeApplicationExportVO> list = overtimeApplicationService.getExportList(reqVO);
|
||||||
|
ExcelUtils.write(response, "加班申请_" + LocalDate.now() + ".xls", "加班申请",
|
||||||
|
OvertimeApplicationExportVO.class, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||||
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ExcelIgnoreUnannotated
|
||||||
|
public class OvertimeApplicationExportVO {
|
||||||
|
|
||||||
|
@ExcelProperty("申请人")
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
@ExcelProperty("加班日期")
|
||||||
|
private LocalDate overtimeDate;
|
||||||
|
|
||||||
|
@ExcelProperty("加班时长")
|
||||||
|
private String overtimeDuration;
|
||||||
|
|
||||||
|
@ExcelProperty("加班原因")
|
||||||
|
private String overtimeReason;
|
||||||
|
|
||||||
|
@ExcelProperty("加班内容")
|
||||||
|
private String overtimeContent;
|
||||||
|
|
||||||
|
@ExcelProperty("状态")
|
||||||
|
private String statusName;
|
||||||
|
|
||||||
|
@ExcelProperty("审核人")
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
@ExcelProperty("提交时间")
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
@ExcelProperty("审核时间")
|
||||||
|
private LocalDateTime approvalTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OvertimeApplicationPageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "关键词,匹配加班原因或加班内容", example = "上线")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "申请人姓名,模糊匹配", example = "张三")
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
@Schema(description = "审核人用户编号", example = "1001")
|
||||||
|
private Long approverId;
|
||||||
|
|
||||||
|
@Schema(description = "审核人姓名,模糊匹配", example = "李四")
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
@Schema(description = "状态编码", example = "pending")
|
||||||
|
@Size(max = 32, message = "状态编码长度不能超过32个字符")
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@Schema(description = "加班日期范围", example = "[2026-06-01, 2026-06-30]")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
|
||||||
|
private LocalDate[] overtimeDate;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "[2026-06-01 00:00:00, 2026-06-30 23:59:59]")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请 Response VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "加班申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "申请人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||||
|
private Long applicantId;
|
||||||
|
|
||||||
|
@Schema(description = "申请人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
@Schema(description = "加班日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDate overtimeDate;
|
||||||
|
|
||||||
|
@Schema(description = "加班时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "1天")
|
||||||
|
private String overtimeDuration;
|
||||||
|
|
||||||
|
@Schema(description = "加班原因", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String overtimeReason;
|
||||||
|
|
||||||
|
@Schema(description = "加班内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String overtimeContent;
|
||||||
|
|
||||||
|
@Schema(description = "审核人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||||
|
private Long approverId;
|
||||||
|
|
||||||
|
@Schema(description = "审核人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@Schema(description = "状态名称", example = "待审批")
|
||||||
|
private String statusName;
|
||||||
|
|
||||||
|
@Schema(description = "当前状态是否允许编辑", example = "false")
|
||||||
|
private Boolean allowEdit;
|
||||||
|
|
||||||
|
@Schema(description = "是否终态", example = "false")
|
||||||
|
private Boolean terminal;
|
||||||
|
|
||||||
|
@Schema(description = "最近一次审核意见")
|
||||||
|
private String approvalComment;
|
||||||
|
|
||||||
|
@Schema(description = "提交时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
@Schema(description = "最近一次审核时间")
|
||||||
|
private LocalDateTime approvalTime;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请保存 Request VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationSaveReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "加班日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-01")
|
||||||
|
@NotNull(message = "加班日期不能为空")
|
||||||
|
private LocalDate overtimeDate;
|
||||||
|
|
||||||
|
@Schema(description = "加班时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "1天")
|
||||||
|
@NotBlank(message = "加班时长不能为空")
|
||||||
|
@Size(max = 30, message = "加班时长长度不能超过30个字符")
|
||||||
|
private String overtimeDuration;
|
||||||
|
|
||||||
|
@Schema(description = "加班原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "版本上线保障")
|
||||||
|
@NotBlank(message = "加班原因不能为空")
|
||||||
|
@Size(max = 500, message = "加班原因长度不能超过500个字符")
|
||||||
|
private String overtimeReason;
|
||||||
|
|
||||||
|
@Schema(description = "加班内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "处理上线验证和问题修复")
|
||||||
|
@NotBlank(message = "加班内容不能为空")
|
||||||
|
@Size(max = 1000, message = "加班内容长度不能超过1000个字符")
|
||||||
|
private String overtimeContent;
|
||||||
|
|
||||||
|
@Schema(description = "审核人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||||
|
@NotNull(message = "审核人不能为空")
|
||||||
|
private Long approverId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请状态动作 Request VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationStatusActionReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "动作原因或审核意见。是否必填以状态机配置为准;当前退回必填,撤销选填",
|
||||||
|
example = "请补充加班内容")
|
||||||
|
@Size(max = 1000, message = "动作原因长度不能超过 1000 个字符")
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台 - 加班申请状态字典 Response VO
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 加班申请状态字典 Response VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationStatusDictRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "状态编码", example = "pending")
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@Schema(description = "状态名称", example = "待审批")
|
||||||
|
private String statusName;
|
||||||
|
|
||||||
|
@Schema(description = "排序值", example = "10")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "是否初始状态", example = "true")
|
||||||
|
private Boolean initialFlag;
|
||||||
|
|
||||||
|
@Schema(description = "是否终态", example = "false")
|
||||||
|
private Boolean terminalFlag;
|
||||||
|
|
||||||
|
@Schema(description = "是否允许编辑", example = "false")
|
||||||
|
private Boolean allowEdit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请状态日志 Response VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationStatusLogRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "加班申请编号", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long applicationId;
|
||||||
|
|
||||||
|
@Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "approve")
|
||||||
|
private String actionType;
|
||||||
|
|
||||||
|
@Schema(description = "变更前状态", example = "pending")
|
||||||
|
private String fromStatus;
|
||||||
|
|
||||||
|
@Schema(description = "变更后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "approved")
|
||||||
|
private String toStatus;
|
||||||
|
|
||||||
|
@Schema(description = "原因或审核意见")
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
@Schema(description = "操作人用户编号", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long operatorUserId;
|
||||||
|
|
||||||
|
@Schema(description = "操作人名称", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String operatorName;
|
||||||
|
|
||||||
|
@Schema(description = "申请人姓名快照", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String applicantNameSnapshot;
|
||||||
|
|
||||||
|
@Schema(description = "加班日期快照", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDate overtimeDateSnapshot;
|
||||||
|
|
||||||
|
@Schema(description = "加班时长快照", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String overtimeDurationSnapshot;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -46,14 +46,14 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "创建个人事项")
|
@Operation(summary = "创建个人事项")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_CREATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_CREATE + "')")
|
||||||
public CommonResult<Long> create(@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
public CommonResult<Long> create(@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
||||||
return success(personalItemService.createItem(reqVO));
|
return success(personalItemService.createItem(reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "更新个人事项")
|
@Operation(summary = "更新个人事项")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> update(@PathVariable("id") Long id,
|
public CommonResult<Boolean> update(@PathVariable("id") Long id,
|
||||||
@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
||||||
personalItemService.updateItem(id, reqVO);
|
personalItemService.updateItem(id, reqVO);
|
||||||
@@ -62,21 +62,21 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "获取个人事项详情")
|
@Operation(summary = "获取个人事项详情")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||||
public CommonResult<PersonalItemRespVO> get(@PathVariable("id") Long id) {
|
public CommonResult<PersonalItemRespVO> get(@PathVariable("id") Long id) {
|
||||||
return success(personalItemService.getItemRespVO(id));
|
return success(personalItemService.getItemRespVO(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@Operation(summary = "获取个人事项分页")
|
@Operation(summary = "获取个人事项分页")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||||
public CommonResult<PageResult<PersonalItemRespVO>> page(@Valid PersonalItemPageReqVO reqVO) {
|
public CommonResult<PageResult<PersonalItemRespVO>> page(@Valid PersonalItemPageReqVO reqVO) {
|
||||||
return success(personalItemService.getItemRespVOPage(reqVO));
|
return success(personalItemService.getItemRespVOPage(reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/change-status")
|
@PostMapping("/{id}/change-status")
|
||||||
@Operation(summary = "变更个人事项状态")
|
@Operation(summary = "变更个人事项状态")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_STATUS + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_STATUS + "')")
|
||||||
public CommonResult<Boolean> changeStatus(@PathVariable("id") Long id,
|
public CommonResult<Boolean> changeStatus(@PathVariable("id") Long id,
|
||||||
@Valid @RequestBody PersonalItemStatusActionReqVO reqVO) {
|
@Valid @RequestBody PersonalItemStatusActionReqVO reqVO) {
|
||||||
personalItemService.changeStatus(id, reqVO);
|
personalItemService.changeStatus(id, reqVO);
|
||||||
@@ -85,7 +85,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@PostMapping("/{id}/worklogs")
|
@PostMapping("/{id}/worklogs")
|
||||||
@Operation(summary = "新增个人事项工作日志")
|
@Operation(summary = "新增个人事项工作日志")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Long> createWorklog(@PathVariable("id") Long id,
|
public CommonResult<Long> createWorklog(@PathVariable("id") Long id,
|
||||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||||
return success(personalItemService.createWorklog(id, reqVO));
|
return success(personalItemService.createWorklog(id, reqVO));
|
||||||
@@ -93,7 +93,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@GetMapping("/{id}/worklogs")
|
@GetMapping("/{id}/worklogs")
|
||||||
@Operation(summary = "获取个人事项工作日志分页")
|
@Operation(summary = "获取个人事项工作日志分页")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||||
public CommonResult<PageResult<TaskWorklogRespVO>> getWorklogPage(@PathVariable("id") Long id,
|
public CommonResult<PageResult<TaskWorklogRespVO>> getWorklogPage(@PathVariable("id") Long id,
|
||||||
@Valid TaskWorklogPageReqVO reqVO) {
|
@Valid TaskWorklogPageReqVO reqVO) {
|
||||||
return success(personalItemService.getWorklogPage(id, reqVO));
|
return success(personalItemService.getWorklogPage(id, reqVO));
|
||||||
@@ -101,7 +101,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@PutMapping("/{id}/worklogs/{worklogId}")
|
@PutMapping("/{id}/worklogs/{worklogId}")
|
||||||
@Operation(summary = "修改个人事项工作日志")
|
@Operation(summary = "修改个人事项工作日志")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> updateWorklog(@PathVariable("id") Long id,
|
public CommonResult<Boolean> updateWorklog(@PathVariable("id") Long id,
|
||||||
@PathVariable("worklogId") Long worklogId,
|
@PathVariable("worklogId") Long worklogId,
|
||||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||||
@@ -111,7 +111,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@DeleteMapping("/{id}/worklogs/{worklogId}")
|
@DeleteMapping("/{id}/worklogs/{worklogId}")
|
||||||
@Operation(summary = "删除个人事项工作日志")
|
@Operation(summary = "删除个人事项工作日志")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> deleteWorklog(@PathVariable("id") Long id,
|
public CommonResult<Boolean> deleteWorklog(@PathVariable("id") Long id,
|
||||||
@PathVariable("worklogId") Long worklogId) {
|
@PathVariable("worklogId") Long worklogId) {
|
||||||
personalItemService.deleteWorklog(id, worklogId);
|
personalItemService.deleteWorklog(id, worklogId);
|
||||||
@@ -120,7 +120,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@DeleteMapping("/{id}/worklogs/delete-list")
|
@DeleteMapping("/{id}/worklogs/delete-list")
|
||||||
@Operation(summary = "批量删除个人事项工作日志")
|
@Operation(summary = "批量删除个人事项工作日志")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> deleteWorklogs(@PathVariable("id") Long id,
|
public CommonResult<Boolean> deleteWorklogs(@PathVariable("id") Long id,
|
||||||
@RequestParam("ids") List<Long> ids) {
|
@RequestParam("ids") List<Long> ids) {
|
||||||
personalItemService.deleteWorklogs(id, ids);
|
personalItemService.deleteWorklogs(id, ids);
|
||||||
@@ -129,7 +129,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@DeleteMapping("/delete")
|
@DeleteMapping("/delete")
|
||||||
@Operation(summary = "删除个人事项")
|
@Operation(summary = "删除个人事项")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
||||||
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
|
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
|
||||||
personalItemService.deleteItem(id);
|
personalItemService.deleteItem(id);
|
||||||
return success(true);
|
return success(true);
|
||||||
@@ -137,7 +137,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@DeleteMapping("/delete-list")
|
@DeleteMapping("/delete-list")
|
||||||
@Operation(summary = "批量删除个人事项")
|
@Operation(summary = "批量删除个人事项")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
||||||
public CommonResult<Boolean> deleteList(@RequestParam("ids") List<Long> ids) {
|
public CommonResult<Boolean> deleteList(@RequestParam("ids") List<Long> ids) {
|
||||||
personalItemService.deleteItems(ids);
|
personalItemService.deleteItems(ids);
|
||||||
return success(true);
|
return success(true);
|
||||||
@@ -145,7 +145,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@PostMapping("/relate-execution")
|
@PostMapping("/relate-execution")
|
||||||
@Operation(summary = "批量个人事项关联执行")
|
@Operation(summary = "批量个人事项关联执行")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> relateExecution(@RequestParam("itemIds") List<Long> itemIds,
|
public CommonResult<Boolean> relateExecution(@RequestParam("itemIds") List<Long> itemIds,
|
||||||
@RequestParam("executionId") Long executionId) {
|
@RequestParam("executionId") Long executionId) {
|
||||||
personalItemService.relateExecution(itemIds, executionId);
|
personalItemService.relateExecution(itemIds, executionId);
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ public class ProductRequirementController {
|
|||||||
@Resource
|
@Resource
|
||||||
private ProductRequirementService requirementService;
|
private ProductRequirementService requirementService;
|
||||||
|
|
||||||
// ========== 需求管理 ==========
|
|
||||||
|
|
||||||
@PostMapping("/create")
|
@PostMapping("/create")
|
||||||
@Operation(summary = "创建产品需求")
|
@Operation(summary = "创建产品需求")
|
||||||
public CommonResult<Long> createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) {
|
public CommonResult<Long> createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) {
|
||||||
@@ -59,11 +57,18 @@ public class ProductRequirementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/tree")
|
@GetMapping("/tree")
|
||||||
@Operation(summary = "获取需求树形列表(分页)")
|
@Operation(summary = "获取需求树分页列表")
|
||||||
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementTree(@Valid ProductRequirementPageReqVO pageReqVO) {
|
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementTree(@Valid ProductRequirementPageReqVO pageReqVO) {
|
||||||
return success(requirementService.getRequirementTree(pageReqVO));
|
return success(requirementService.getRequirementTree(pageReqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dashboard")
|
||||||
|
@Operation(summary = "获取产品需求概览数据")
|
||||||
|
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||||
|
public CommonResult<ProductRequirementDashboardRespVO> getRequirementDashboard(@RequestParam("productId") Long productId) {
|
||||||
|
return success(requirementService.getRequirementDashboard(productId));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/change-status")
|
@PostMapping("/change-status")
|
||||||
@Operation(summary = "变更需求状态")
|
@Operation(summary = "变更需求状态")
|
||||||
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) {
|
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) {
|
||||||
@@ -125,16 +130,6 @@ public class ProductRequirementController {
|
|||||||
return success(requirementService.hasDispatchedProjectRequirementBatch(reqVO));
|
return success(requirementService.hasDispatchedProjectRequirementBatch(reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/lifecycle")
|
|
||||||
@Operation(summary = "获取需求生命周期信息")
|
|
||||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
|
|
||||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
|
||||||
public CommonResult<ProductRequirementLifecycleRespVO> getRequirementLifecycle(
|
|
||||||
@RequestParam("requirementId") Long requirementId,
|
|
||||||
@RequestParam("productId") Long productId) {
|
|
||||||
return success(requirementService.getRequirementLifecycle(requirementId, productId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/dispatched-project-link")
|
@GetMapping("/dispatched-project-link")
|
||||||
@Operation(summary = "获取产品需求分流后对应的项目需求跳转链接")
|
@Operation(summary = "获取产品需求分流后对应的项目需求跳转链接")
|
||||||
@Parameter(name = "productRequirementId", description = "产品需求编号", required = true, example = "1024")
|
@Parameter(name = "productRequirementId", description = "产品需求编号", required = true, example = "1024")
|
||||||
@@ -143,7 +138,6 @@ public class ProductRequirementController {
|
|||||||
return success(requirementService.getDispatchedProjectLink(productRequirementId));
|
return success(requirementService.getDispatchedProjectLink(productRequirementId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块管理 ==========
|
|
||||||
@PostMapping("/module/create")
|
@PostMapping("/module/create")
|
||||||
@Operation(summary = "创建需求模块")
|
@Operation(summary = "创建需求模块")
|
||||||
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
|
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台 - 产品需求概览最近变化 Response VO
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 产品需求概览最近变化 Response VO")
|
||||||
|
@Data
|
||||||
|
public class ProductRequirementDashboardRecentChangeRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "前端列表唯一键", requiredMode = Schema.RequiredMode.REQUIRED, example = "requirement:create:2048")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "产品需求ID", example = "1003")
|
||||||
|
private Long requirementId;
|
||||||
|
|
||||||
|
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "补充对象首页需求池统计接口")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "动作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "create")
|
||||||
|
private String actionType;
|
||||||
|
|
||||||
|
@Schema(description = "动作名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "需求新增")
|
||||||
|
private String actionLabel;
|
||||||
|
|
||||||
|
@Schema(description = "展示内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "当前状态:待评审")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "发生时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime occurredAt;
|
||||||
|
|
||||||
|
@Schema(description = "操作人用户ID", example = "1024")
|
||||||
|
private Long operatorUserId;
|
||||||
|
|
||||||
|
@Schema(description = "操作人名称快照", example = "张三")
|
||||||
|
private String operatorName;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台 - 产品需求概览 Response VO
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 产品需求概览 Response VO")
|
||||||
|
@Data
|
||||||
|
public class ProductRequirementDashboardRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "需求池统计")
|
||||||
|
private ProductRequirementDashboardSummaryRespVO summary;
|
||||||
|
|
||||||
|
@Schema(description = "需求池最近重要变化")
|
||||||
|
private List<ProductRequirementDashboardRecentChangeRespVO> recentChanges;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台 - 产品需求概览统计 Response VO
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 产品需求概览统计 Response VO")
|
||||||
|
@Data
|
||||||
|
public class ProductRequirementDashboardSummaryRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "需求总量,包括根需求和子需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
|
||||||
|
private Long total;
|
||||||
|
|
||||||
|
@Schema(description = "待处理需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||||
|
private Long todo;
|
||||||
|
|
||||||
|
@Schema(description = "待认领需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Long pendingClaim;
|
||||||
|
|
||||||
|
@Schema(description = "待评审需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Long pendingReview;
|
||||||
|
|
||||||
|
@Schema(description = "待指派需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Long pendingDispatch;
|
||||||
|
|
||||||
|
@Schema(description = "完成需求数,已验收和已关闭计入完成", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
|
||||||
|
private Long completed;
|
||||||
|
|
||||||
|
@Schema(description = "完成率,四舍五入后的百分比整数", requiredMode = Schema.RequiredMode.REQUIRED, example = "33")
|
||||||
|
private Integer completionRate;
|
||||||
|
|
||||||
|
@Schema(description = "高优先待处理需求数,P0/P1 且处于待处理状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||||
|
private Long highPriorityTodo;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理后台 - 产品需求分流后项目需求跳转链接 Response VO
|
* 管理后台 - 产品需求指派后项目需求跳转链接 Response VO
|
||||||
*/
|
*/
|
||||||
@Schema(description = "管理后台 - 产品需求分流后项目需求跳转链接 Response VO")
|
@Schema(description = "管理后台 - 产品需求指派后项目需求跳转链接 Response VO")
|
||||||
@Data
|
@Data
|
||||||
public class ProductRequirementDispatchedProjectLinkRespVO {
|
public class ProductRequirementDispatchedProjectLinkRespVO {
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Schema(description = "管理后台 - 产品需求批量分流状态 Response VO")
|
@Schema(description = "管理后台 - 产品需求批量指派状态 Response VO")
|
||||||
@Data
|
@Data
|
||||||
public class ProductRequirementHasDispatchedBatchRespVO {
|
public class ProductRequirementHasDispatchedBatchRespVO {
|
||||||
|
|
||||||
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
private Long requirementId;
|
private Long requirementId;
|
||||||
|
|
||||||
@Schema(description = "是否已分流生成项目需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
@Schema(description = "是否已指派生成项目需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||||
private Boolean hasDispatched;
|
private Boolean hasDispatched;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理后台 - 产品需求生命周期 Response VO
|
|
||||||
*/
|
|
||||||
@Schema(description = "管理后台 - 产品需求生命周期 Response VO")
|
|
||||||
@Data
|
|
||||||
public class ProductRequirementLifecycleRespVO {
|
|
||||||
|
|
||||||
@Schema(description = "当前状态编码", example = "pending_dispatch")
|
|
||||||
private String statusCode;
|
|
||||||
|
|
||||||
@Schema(description = "当前状态名称", example = "待分流")
|
|
||||||
private String statusName;
|
|
||||||
|
|
||||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
|
||||||
private String lastStatusReason;
|
|
||||||
|
|
||||||
@Schema(description = "是否终态", example = "false")
|
|
||||||
private Boolean terminal;
|
|
||||||
|
|
||||||
@Schema(description = "是否允许编辑", example = "true")
|
|
||||||
private Boolean allowEdit;
|
|
||||||
|
|
||||||
@Schema(description = "当前状态可执行动作列表")
|
|
||||||
private List<ProductRequirementStatusTransitionRespVO> availableActions;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -54,10 +55,10 @@ public class ProductRequirementRespVO {
|
|||||||
@Schema(description = "当前状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending_dispatch")
|
@Schema(description = "当前状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending_dispatch")
|
||||||
private String statusCode;
|
private String statusCode;
|
||||||
|
|
||||||
@Schema(description = "当前状态名称", example = "待分流")
|
@Schema(description = "当前状态名称", example = "待指派")
|
||||||
private String statusName;
|
private String statusName;
|
||||||
|
|
||||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
@Schema(description = "最近一次状态动作原因", example = "需求全部结束")
|
||||||
private String lastStatusReason;
|
private String lastStatusReason;
|
||||||
|
|
||||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
@@ -95,8 +96,4 @@ public class ProductRequirementRespVO {
|
|||||||
|
|
||||||
@Schema(description = "子需求列表,树形结构")
|
@Schema(description = "子需求列表,树形结构")
|
||||||
private List<ProductRequirementRespVO> children;
|
private List<ProductRequirementRespVO> children;
|
||||||
|
|
||||||
@Schema(description = "是否为终态", example = "false")
|
|
||||||
private Boolean terminal;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class ProductRequirementStatusActionReqVO {
|
|||||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||||
private String actionCode;
|
private String actionCode;
|
||||||
|
|
||||||
@Schema(description = "状态变更原因", example = "评审通过,进入分流阶段")
|
@Schema(description = "状态变更原因", example = "需求全部完成")
|
||||||
private String reason;
|
private String reason;
|
||||||
|
|
||||||
@Schema(description = "关联项目编号(dispatch动作时可选)", example = "1024")
|
@Schema(description = "关联项目编号(dispatch动作时可选)", example = "1024")
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class ProductRequirementStatusDictRespVO {
|
public class ProductRequirementStatusDictRespVO {
|
||||||
|
|
||||||
@Schema(description = "状态编码", example = "pending_confirm")
|
@Schema(description = "状态编码", example = "pending_claim")
|
||||||
private String statusCode;
|
private String statusCode;
|
||||||
|
|
||||||
@Schema(description = "状态名称", example = "待确认")
|
@Schema(description = "状态名称", example = "待认领")
|
||||||
private String statusName;
|
private String statusName;
|
||||||
|
|
||||||
@Schema(description = "排序值", example = "1")
|
@Schema(description = "排序值", example = "1")
|
||||||
@@ -25,4 +25,7 @@ public class ProductRequirementStatusDictRespVO {
|
|||||||
@Schema(description = "是否终态", example = "false")
|
@Schema(description = "是否终态", example = "false")
|
||||||
private Boolean terminalFlag;
|
private Boolean terminalFlag;
|
||||||
|
|
||||||
|
@Schema(description = "是否允许编辑", example = "true")
|
||||||
|
private Boolean allowEdit;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class ProductRequirementStatusTransitionRespVO {
|
|||||||
@Schema(description = "动作编码", example = "dispatch")
|
@Schema(description = "动作编码", example = "dispatch")
|
||||||
private String actionCode;
|
private String actionCode;
|
||||||
|
|
||||||
@Schema(description = "动作名称", example = "明确分流/拆分")
|
@Schema(description = "动作名称", example = "明确指派/拆分")
|
||||||
private String actionName;
|
private String actionName;
|
||||||
|
|
||||||
@Schema(description = "目标状态编码", example = "implementing")
|
@Schema(description = "目标状态编码", example = "implementing")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.Proj
|
|||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementDeleteReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementDeleteReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementLifecycleRespVO;
|
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleDeleteReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleDeleteReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO;
|
||||||
@@ -72,7 +71,7 @@ public class ProjectRequirementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/tree")
|
@GetMapping("/tree")
|
||||||
@Operation(summary = "获取需求树形列表")
|
@Operation(summary = "获取需求树分页列表")
|
||||||
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementTree(@Valid ProjectRequirementPageReqVO pageReqVO) {
|
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementTree(@Valid ProjectRequirementPageReqVO pageReqVO) {
|
||||||
return success(requirementService.getRequirementTree(pageReqVO));
|
return success(requirementService.getRequirementTree(pageReqVO));
|
||||||
}
|
}
|
||||||
@@ -121,16 +120,6 @@ public class ProjectRequirementController {
|
|||||||
return success(requirementService.getAllowedTransitionsBatch(reqVO));
|
return success(requirementService.getAllowedTransitionsBatch(reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/lifecycle")
|
|
||||||
@Operation(summary = "获取需求生命周期信息")
|
|
||||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
|
|
||||||
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
|
|
||||||
public CommonResult<ProjectRequirementLifecycleRespVO> getRequirementLifecycle(
|
|
||||||
@RequestParam("requirementId") Long requirementId,
|
|
||||||
@RequestParam("projectId") Long projectId) {
|
|
||||||
return success(requirementService.getRequirementLifecycle(requirementId, projectId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/module/create")
|
@PostMapping("/module/create")
|
||||||
@Operation(summary = "创建需求模块")
|
@Operation(summary = "创建需求模块")
|
||||||
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) {
|
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) {
|
||||||
@@ -165,7 +154,7 @@ public class ProjectRequirementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/status/dict/terminal")
|
@GetMapping("/status/dict/terminal")
|
||||||
@Operation(summary = "获取需求终态状态字典")
|
@Operation(summary = "获取需求终止态状态字典")
|
||||||
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
|
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
|
||||||
return success(requirementService.getRequirementTerminalStatusDict());
|
return success(requirementService.getRequirementTerminalStatusDict());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.MyProjectExecutionRespVO;
|
||||||
|
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 我负责的执行(跨项目)")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/project/project/me/executions")
|
||||||
|
@Validated
|
||||||
|
public class MyExecutionController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ProjectExecutionService projectExecutionService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "分页获取当前登录用户负责的执行(跨项目,默认排除终态与进度满)")
|
||||||
|
public CommonResult<PageResult<MyProjectExecutionRespVO>> getMyExecutionPage(@Valid MyProjectExecutionPageReqVO reqVO) {
|
||||||
|
// 前端固定传 pageSize=-1 拉全部;负数统一归一为 PAGE_SIZE_NONE(-1),与现有执行分页接口一致
|
||||||
|
if (reqVO.getPageSize() != null && reqVO.getPageSize() < 0) {
|
||||||
|
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||||
|
}
|
||||||
|
return success(projectExecutionService.getMyExecutionPage(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
||||||
|
|
||||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||||
@@ -65,6 +66,11 @@ public class ProjectExecutionController {
|
|||||||
@Operation(summary = "获取执行分页")
|
@Operation(summary = "获取执行分页")
|
||||||
public CommonResult<PageResult<ProjectExecutionRespVO>> getExecutionPage(@PathVariable("projectId") Long projectId,
|
public CommonResult<PageResult<ProjectExecutionRespVO>> getExecutionPage(@PathVariable("projectId") Long projectId,
|
||||||
@Valid ProjectExecutionPageReqVO reqVO) {
|
@Valid ProjectExecutionPageReqVO reqVO) {
|
||||||
|
// 前端用负数 pageSize 表示"查询全部",统一归一为框架约定的 PAGE_SIZE_NONE(-1),
|
||||||
|
// 走 BaseMapperX.selectPage 的不分页短路;@Max(200) 仅拦上界,负数不会被 validator 卡。
|
||||||
|
if (reqVO.getPageSize() != null && reqVO.getPageSize() < 0) {
|
||||||
|
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||||
|
}
|
||||||
return success(projectExecutionService.getExecutionRespVOPage(projectId, reqVO));
|
return success(projectExecutionService.getExecutionRespVOPage(projectId, reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 我负责的执行(跨项目)分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class MyProjectExecutionPageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "执行状态编码(预留,单状态精确过滤)", example = "active")
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@Schema(description = "执行名称模糊匹配关键字(预留)", example = "联调")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 我负责的执行(跨项目)Response VO")
|
||||||
|
@Data
|
||||||
|
public class MyProjectExecutionRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
||||||
|
private Long id;
|
||||||
|
@Schema(description = "执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调")
|
||||||
|
private String executionName;
|
||||||
|
@Schema(description = "所属项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001")
|
||||||
|
private Long projectId;
|
||||||
|
@Schema(description = "所属项目名称", example = "商城 V2 升级")
|
||||||
|
private String projectName;
|
||||||
|
@Schema(description = "执行状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||||
|
private String statusCode;
|
||||||
|
@Schema(description = "执行状态名称", example = "进行中")
|
||||||
|
private String statusName;
|
||||||
|
@Schema(description = "优先级编码(字典 rdms_req_priority),0=P0(最高) ~ 3=P3(最低)", example = "0")
|
||||||
|
private String priority;
|
||||||
|
@Schema(description = "计划开始日期")
|
||||||
|
private LocalDate plannedStartDate;
|
||||||
|
@Schema(description = "计划结束日期")
|
||||||
|
private LocalDate plannedEndDate;
|
||||||
|
@Schema(description = "实际开始日期")
|
||||||
|
private LocalDate actualStartDate;
|
||||||
|
@Schema(description = "实际结束日期")
|
||||||
|
private LocalDate actualEndDate;
|
||||||
|
@Schema(description = "执行进度百分比 0-100", example = "68")
|
||||||
|
private Integer progressRate;
|
||||||
|
@Schema(description = "关联项目需求编号")
|
||||||
|
private Long projectRequirementId;
|
||||||
|
@Schema(description = "关联项目需求名称", example = "订单履约后端拆分(一期)")
|
||||||
|
private String projectRequirementName;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -23,7 +23,10 @@ public class ProjectExecutionPageReqVO extends PageParam {
|
|||||||
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
||||||
private String executionType;
|
private String executionType;
|
||||||
|
|
||||||
@Schema(description = "执行负责人用户编号", example = "3001")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 项目内全部执行")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3001")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "执行状态编码", example = "pending")
|
@Schema(description = "执行状态编码", example = "pending")
|
||||||
@@ -38,4 +41,8 @@ public class ProjectExecutionPageReqVO extends PageParam {
|
|||||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
private LocalDateTime[] updateTime;
|
private LocalDateTime[] updateTime;
|
||||||
|
|
||||||
|
@Schema(description = "截止时间范围 chip:overdue(逾期)/ today(今天到期)/ thisWeek(本周到期);" +
|
||||||
|
"基于 plannedEndDate 且排除终态执行;不传 = 不按截止时间过滤", example = "overdue")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,18 @@ public class ProjectExecutionStatusBoardReqVO {
|
|||||||
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
||||||
private String executionType;
|
private String executionType;
|
||||||
|
|
||||||
@Schema(description = "执行负责人用户编号", example = "3001")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 项目内全部执行")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3001")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
||||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
private LocalDateTime[] updateTime;
|
private LocalDateTime[] updateTime;
|
||||||
|
|
||||||
|
@Schema(description = "截止时间范围 chip:overdue(逾期)/ today(今天到期)/ thisWeek(本周到期);" +
|
||||||
|
"基于 plannedEndDate 且排除终态执行;不传 = 不按截止时间过滤", example = "overdue")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.project;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectOwnedRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectParticipatedRespVO;
|
||||||
|
import com.njcn.rdms.module.project.service.project.MyProjectService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 工作台「我的项目」")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/project/project/me")
|
||||||
|
@Validated
|
||||||
|
public class MyProjectController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private MyProjectService myProjectService;
|
||||||
|
|
||||||
|
@GetMapping("/participated/page")
|
||||||
|
@Operation(summary = "分页获取当前登录用户参与的项目(作为成员)")
|
||||||
|
public CommonResult<PageResult<MyProjectParticipatedRespVO>> getMyParticipatedPage(@Valid MyProjectPageReqVO reqVO) {
|
||||||
|
normalizePageSize(reqVO);
|
||||||
|
return success(myProjectService.getMyParticipatedPage(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/owned/page")
|
||||||
|
@Operation(summary = "分页获取当前登录用户负责的项目(managerUserId=当前用户)")
|
||||||
|
public CommonResult<PageResult<MyProjectOwnedRespVO>> getMyOwnedPage(@Valid MyProjectPageReqVO reqVO) {
|
||||||
|
normalizePageSize(reqVO);
|
||||||
|
return success(myProjectService.getMyOwnedPage(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 前端固定传 pageSize=-1 拉全部;负数统一归一为 PAGE_SIZE_NONE,与 MyExecutionController 一致。 */
|
||||||
|
private void normalizePageSize(MyProjectPageReqVO reqVO) {
|
||||||
|
if (reqVO.getPageSize() != null && reqVO.getPageSize() < 0) {
|
||||||
|
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 我负责的项目 Response VO")
|
||||||
|
@Data
|
||||||
|
public class MyProjectOwnedRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001")
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long id;
|
||||||
|
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "商城 V2 升级")
|
||||||
|
private String name;
|
||||||
|
@Schema(description = "项目编码", example = "MALL-V2")
|
||||||
|
private String code;
|
||||||
|
@Schema(description = "项目整体进度百分比 0-100", requiredMode = Schema.RequiredMode.REQUIRED, example = "70")
|
||||||
|
private Integer progress;
|
||||||
|
@Schema(description = "当前用户在该项目中的角色名(恒含负责人语义)", example = "项目负责人")
|
||||||
|
private String myRole;
|
||||||
|
@Schema(description = "项目计划结束日期 YYYY-MM-DD;未设为 null")
|
||||||
|
private LocalDate plannedEndDate;
|
||||||
|
@Schema(description = "项目下进行中执行数", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
|
||||||
|
private Integer executionCount;
|
||||||
|
@Schema(description = "项目下进行中任务数", requiredMode = Schema.RequiredMode.REQUIRED, example = "24")
|
||||||
|
private Integer taskCount;
|
||||||
|
@Schema(description = "项目当前有效成员数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||||
|
private Integer memberCount;
|
||||||
|
@Schema(description = "项目下逾期任务数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||||
|
private Integer overdueCount;
|
||||||
|
@Schema(description = "成员负载原始数据;无成员为 []", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<MemberLoadVO> members;
|
||||||
|
|
||||||
|
@Schema(description = "成员负载原始数据")
|
||||||
|
@Data
|
||||||
|
public static class MemberLoadVO {
|
||||||
|
@Schema(description = "成员用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "101")
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long userId;
|
||||||
|
@Schema(description = "成员姓名/昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
|
||||||
|
private String userName;
|
||||||
|
@Schema(description = "该成员在本项目下的进行中任务数", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
|
||||||
|
private Integer activeTaskCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 工作台「我的项目」分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class MyProjectPageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "项目名称/编码模糊匹配关键字(预留,本期不过滤)", example = "商城")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 我参与的项目 Response VO")
|
||||||
|
@Data
|
||||||
|
public class MyProjectParticipatedRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001")
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
private Long id;
|
||||||
|
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "商城 V2 升级")
|
||||||
|
private String name;
|
||||||
|
@Schema(description = "项目编码", example = "MALL-V2")
|
||||||
|
private String code;
|
||||||
|
@Schema(description = "项目状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||||
|
private String statusCode;
|
||||||
|
@Schema(description = "项目状态名称", example = "进行中")
|
||||||
|
private String statusName;
|
||||||
|
@Schema(description = "项目整体进度百分比 0-100", requiredMode = Schema.RequiredMode.REQUIRED, example = "70")
|
||||||
|
private Integer progress;
|
||||||
|
@Schema(description = "当前用户在该项目中的角色名(主角色 / 附加角色拼接)", example = "前端负责人")
|
||||||
|
private String myRole;
|
||||||
|
@Schema(description = "我负责的任务总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8")
|
||||||
|
private Integer myTaskCount;
|
||||||
|
@Schema(description = "我负责的未完成任务数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||||
|
private Integer myPendingTaskCount;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateBoardPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryRespVO;
|
||||||
|
import com.njcn.rdms.module.project.service.project.task.ProjectTaskAggregateService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 项目级跨执行任务查询")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/project/project/{projectId}/tasks")
|
||||||
|
@Validated
|
||||||
|
public class ProjectTaskAggregateController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ProjectTaskAggregateService projectTaskAggregateService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "获取项目级跨执行任务分页")
|
||||||
|
public CommonResult<PageResult<ProjectTaskRespVO>> getTaskPage(
|
||||||
|
@PathVariable("projectId") Long projectId,
|
||||||
|
@Valid ProjectTaskAggregatePageReqVO reqVO) {
|
||||||
|
return success(projectTaskAggregateService.getAggregateTaskPage(projectId, reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/status-board")
|
||||||
|
@Operation(summary = "获取项目级跨执行任务状态看板")
|
||||||
|
public CommonResult<ProjectTaskStatusBoardRespVO> getTaskStatusBoard(
|
||||||
|
@PathVariable("projectId") Long projectId,
|
||||||
|
@Valid ProjectTaskAggregateStatusBoardReqVO reqVO) {
|
||||||
|
return success(projectTaskAggregateService.getAggregateTaskStatusBoard(projectId, reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/board-page")
|
||||||
|
@Operation(summary = "获取项目级跨执行任务看板分页")
|
||||||
|
public CommonResult<ProjectTaskBoardPageRespVO> getTaskBoardPage(
|
||||||
|
@PathVariable("projectId") Long projectId,
|
||||||
|
@Valid ProjectTaskAggregateBoardPageReqVO reqVO) {
|
||||||
|
return success(projectTaskAggregateService.getAggregateTaskBoardPage(projectId, reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/summary")
|
||||||
|
@Operation(summary = "获取项目任务今日小条(involveUserId 控制是否限定 owner / 活跃协办)")
|
||||||
|
public CommonResult<ProjectTaskSummaryRespVO> getTaskSummary(
|
||||||
|
@PathVariable("projectId") Long projectId,
|
||||||
|
@Valid ProjectTaskSummaryReqVO reqVO) {
|
||||||
|
return success(projectTaskAggregateService.getAggregateTaskSummary(projectId, reqVO));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,10 @@ public class ProjectTaskBoardPageReqVO extends PageParam {
|
|||||||
@Schema(description = "父任务编号", example = "9001")
|
@Schema(description = "父任务编号", example = "9001")
|
||||||
private Long parentTaskId;
|
private Long parentTaskId;
|
||||||
|
|
||||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
|
@Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ public class ProjectTaskPageReqVO extends PageParam {
|
|||||||
@Schema(description = "父任务编号")
|
@Schema(description = "父任务编号")
|
||||||
private Long parentTaskId;
|
private Long parentTaskId;
|
||||||
|
|
||||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "任务状态编码", example = "pending")
|
@Schema(description = "任务状态编码", example = "pending")
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ public class ProjectTaskRespVO {
|
|||||||
private Long projectId;
|
private Long projectId;
|
||||||
@Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
@Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
||||||
private Long executionId;
|
private Long executionId;
|
||||||
|
@Schema(description = "所属执行名称", example = "迭代 V1.0")
|
||||||
|
private String executionName;
|
||||||
|
@Schema(description = "所属执行状态码")
|
||||||
|
private String executionStatusCode;
|
||||||
@Schema(description = "所属执行关联的项目需求编号(service 层批量回填)")
|
@Schema(description = "所属执行关联的项目需求编号(service 层批量回填)")
|
||||||
private Long projectRequirementId;
|
private Long projectRequirementId;
|
||||||
@Schema(description = "项目需求名称(service 层批量回填,避免 N+1)", example = "前端联调-需求A")
|
@Schema(description = "项目需求名称(service 层批量回填,避免 N+1)", example = "前端联调-需求A")
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ public class ProjectTaskStatusBoardReqVO {
|
|||||||
@Schema(description = "父任务编号", example = "9001")
|
@Schema(description = "父任务编号", example = "9001")
|
||||||
private Long parentTaskId;
|
private Long parentTaskId;
|
||||||
|
|
||||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目级跨执行任务看板分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ProjectTaskAggregateBoardPageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "任务名称模糊匹配关键字")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行")
|
||||||
|
private List<Long> executionIds;
|
||||||
|
|
||||||
|
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||||
|
private List<String> executionStatusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "我参与语义;与 ownerId 二选一")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空")
|
||||||
|
private Long executionInvolveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一")
|
||||||
|
private Long ownerId;
|
||||||
|
|
||||||
|
@Schema(description = "限定状态列(板上显示哪些列);空 / 不传 = 字典全状态")
|
||||||
|
private List<String> statusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "优先级")
|
||||||
|
@Size(max = 8)
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
@Schema(description = "父任务 id")
|
||||||
|
private Long parentTaskId;
|
||||||
|
|
||||||
|
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间范围")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目级跨执行任务分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ProjectTaskAggregatePageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "任务名称模糊匹配关键字")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行")
|
||||||
|
private List<Long> executionIds;
|
||||||
|
|
||||||
|
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||||
|
private List<String> executionStatusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空")
|
||||||
|
private Long executionInvolveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一")
|
||||||
|
private Long ownerId;
|
||||||
|
|
||||||
|
@Schema(description = "状态码多选;空 / 不传 = 全部")
|
||||||
|
private List<String> statusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "优先级字典 value (0~3)")
|
||||||
|
@Size(max = 8)
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
@Schema(description = "父任务 id(限定到某父任务下)")
|
||||||
|
private Long parentTaskId;
|
||||||
|
|
||||||
|
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间范围(2 长度数组)")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] updateTime;
|
||||||
|
|
||||||
|
@Schema(description = "排序字段:plannedEndDate / priority / updateTime / createTime(默认 plannedEndDate)")
|
||||||
|
private String sortBy;
|
||||||
|
|
||||||
|
@Schema(description = "排序方向:asc / desc(默认 asc)")
|
||||||
|
private String sortOrder;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目级跨执行任务状态看板 Request VO(入参同 page 去掉分页 / statusCodes / sort)")
|
||||||
|
@Data
|
||||||
|
public class ProjectTaskAggregateStatusBoardReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "任务名称模糊匹配关键字")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行")
|
||||||
|
private List<Long> executionIds;
|
||||||
|
|
||||||
|
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||||
|
private List<String> executionStatusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "我参与语义;与 ownerId 二选一")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空")
|
||||||
|
private Long executionInvolveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一")
|
||||||
|
private Long ownerId;
|
||||||
|
|
||||||
|
@Schema(description = "优先级")
|
||||||
|
@Size(max = 8)
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
@Schema(description = "父任务 id")
|
||||||
|
private Long parentTaskId;
|
||||||
|
|
||||||
|
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间范围")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目任务今日小条 Request VO")
|
||||||
|
@Data
|
||||||
|
public class ProjectTaskSummaryReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我参与语义:传入的 userId 是 owner 或活跃协办;不传 = 项目内全部任务。
|
||||||
|
* 切换"我参与 / 所有"由前端直接控制此字段是否携带,与其他读接口(page / status-board / board-page)契约一致。
|
||||||
|
*/
|
||||||
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;不传 = 项目内全部")
|
||||||
|
private Long involveUserId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目任务今日小条 Response VO")
|
||||||
|
@Data
|
||||||
|
public class ProjectTaskSummaryRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "逾期任务数:计划完成日 < 今天,且任务状态非终态")
|
||||||
|
private Long overdue;
|
||||||
|
|
||||||
|
@Schema(description = "今日截止任务数:计划完成日 = 今天,且任务状态非终态")
|
||||||
|
private Long dueToday;
|
||||||
|
|
||||||
|
@Schema(description = "本周到期任务数:计划完成日 ∈ [本周一, 本周日],且任务状态非终态(与 chip thisWeek 过滤同口径)")
|
||||||
|
private Long dueThisWeek;
|
||||||
|
|
||||||
|
@Schema(description = "本周已完成任务数:actualEndDate ∈ [本周一, 今天],且任务状态 = completed")
|
||||||
|
private Long doneThisWeek;
|
||||||
|
|
||||||
|
@Schema(description = "服务器当日(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-22")
|
||||||
|
private LocalDate today;
|
||||||
|
|
||||||
|
@Schema(description = "服务器本周一(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-18")
|
||||||
|
private LocalDate weekStart;
|
||||||
|
|
||||||
|
@Schema(description = "服务器本周日(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-24")
|
||||||
|
private LocalDate weekEnd;
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.njcn.rdms.module.project.controller.admin.project.vo.requirement;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理后台 - 项目需求生命周期 Response VO
|
|
||||||
*/
|
|
||||||
@Schema(description = "管理后台 - 项目需求生命周期 Response VO")
|
|
||||||
@Data
|
|
||||||
public class ProjectRequirementLifecycleRespVO {
|
|
||||||
|
|
||||||
@Schema(description = "当前状态编码", example = "implementing")
|
|
||||||
private String statusCode;
|
|
||||||
|
|
||||||
@Schema(description = "当前状态名称", example = "实施中")
|
|
||||||
private String statusName;
|
|
||||||
|
|
||||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
|
||||||
private String lastStatusReason;
|
|
||||||
|
|
||||||
@Schema(description = "是否终态", example = "false")
|
|
||||||
private Boolean terminal;
|
|
||||||
|
|
||||||
@Schema(description = "是否允许编辑", example = "true")
|
|
||||||
private Boolean allowEdit;
|
|
||||||
|
|
||||||
@Schema(description = "当前状态可执行动作列表")
|
|
||||||
private List<ProjectRequirementStatusTransitionRespVO> availableActions;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -61,7 +61,7 @@ public class ProjectRequirementRespVO {
|
|||||||
@Schema(description = "当前状态名称", example = "实施中")
|
@Schema(description = "当前状态名称", example = "实施中")
|
||||||
private String statusName;
|
private String statusName;
|
||||||
|
|
||||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
@Schema(description = "最近一次状态动作原因", example = "需求全部结束")
|
||||||
private String lastStatusReason;
|
private String lastStatusReason;
|
||||||
|
|
||||||
@Schema(description = "提出人用户ID", example = "1024")
|
@Schema(description = "提出人用户ID", example = "1024")
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class ProjectRequirementStatusActionReqVO {
|
|||||||
@Size(max = 32, message = "动作编码长度不能超过 32 个字符")
|
@Size(max = 32, message = "动作编码长度不能超过 32 个字符")
|
||||||
private String actionCode;
|
private String actionCode;
|
||||||
|
|
||||||
@Schema(description = "状态变更原因", example = "评审通过")
|
@Schema(description = "状态变更原因", example = "需求全部结束")
|
||||||
private String reason;
|
private String reason;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class ProjectRequirementStatusDictRespVO {
|
public class ProjectRequirementStatusDictRespVO {
|
||||||
|
|
||||||
@Schema(description = "状态编码", example = "pending_confirm")
|
@Schema(description = "状态编码", example = "pending_claim")
|
||||||
private String statusCode;
|
private String statusCode;
|
||||||
|
|
||||||
@Schema(description = "状态名称", example = "待确认")
|
@Schema(description = "状态名称", example = "待认领")
|
||||||
private String statusName;
|
private String statusName;
|
||||||
|
|
||||||
@Schema(description = "排序值", example = "1")
|
@Schema(description = "排序值", example = "1")
|
||||||
@@ -25,4 +25,7 @@ public class ProjectRequirementStatusDictRespVO {
|
|||||||
@Schema(description = "是否终态", example = "false")
|
@Schema(description = "是否终态", example = "false")
|
||||||
private Boolean terminalFlag;
|
private Boolean terminalFlag;
|
||||||
|
|
||||||
|
@Schema(description = "是否允许编辑", example = "true")
|
||||||
|
private Boolean allowEdit;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class ProjectRequirementStatusTransitionRespVO {
|
|||||||
@Schema(description = "动作编码", example = "pass_review")
|
@Schema(description = "动作编码", example = "pass_review")
|
||||||
private String actionCode;
|
private String actionCode;
|
||||||
|
|
||||||
@Schema(description = "动作名称", example = "评审通过")
|
@Schema(description = "动作名称", example = "通过评审")
|
||||||
private String actionName;
|
private String actionName;
|
||||||
|
|
||||||
@Schema(description = "目标状态编码", example = "implementing")
|
@Schema(description = "目标状态编码", example = "implementing")
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.review;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.review.vo.RequirementReviewRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.review.vo.RequirementReviewSubmitReqVO;
|
||||||
|
import com.njcn.rdms.module.project.service.review.RequirementReviewService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
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.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理后台 - 需求评审控制器
|
||||||
|
*/
|
||||||
|
@Tag(name = "管理后台 - 需求评审")
|
||||||
|
@RestController
|
||||||
|
@Validated
|
||||||
|
public class RequirementReviewController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RequirementReviewService reviewService;
|
||||||
|
|
||||||
|
@PostMapping("/project/product/requirement/review/submit")
|
||||||
|
@Operation(summary = "提交产品需求评审")
|
||||||
|
public CommonResult<Long> submitProductRequirementReview(@Valid @RequestBody RequirementReviewSubmitReqVO reqVO) {
|
||||||
|
return success(reviewService.submitProductRequirementReview(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/project/product/requirement/review/get")
|
||||||
|
@Operation(summary = "获取产品需求评审记录")
|
||||||
|
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||||
|
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "4096")
|
||||||
|
public CommonResult<RequirementReviewRespVO> getProductRequirementReview(@RequestParam("productId") Long productId,
|
||||||
|
@RequestParam("requirementId") Long requirementId) {
|
||||||
|
return success(reviewService.getProductRequirementReview(productId, requirementId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/project/project/requirement/review/submit")
|
||||||
|
@Operation(summary = "提交项目需求评审")
|
||||||
|
public CommonResult<Long> submitProjectRequirementReview(@Valid @RequestBody RequirementReviewSubmitReqVO reqVO) {
|
||||||
|
return success(reviewService.submitProjectRequirementReview(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/project/project/requirement/review/get")
|
||||||
|
@Operation(summary = "获取项目需求评审记录")
|
||||||
|
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
|
||||||
|
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "4096")
|
||||||
|
public CommonResult<RequirementReviewRespVO> getProjectRequirementReview(@RequestParam("projectId") Long projectId,
|
||||||
|
@RequestParam("requirementId") Long requirementId) {
|
||||||
|
return success(reviewService.getProjectRequirementReview(projectId, requirementId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.review.vo;
|
||||||
|
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.review.RequirementReviewAttendeeItem;
|
||||||
|
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
|
||||||
|
public class RequirementReviewRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "评审记录编号", example = "1024")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "对象类型", example = "product_requirement")
|
||||||
|
private String objectType;
|
||||||
|
|
||||||
|
@Schema(description = "需求编号", example = "4096")
|
||||||
|
private Long requirementId;
|
||||||
|
|
||||||
|
@Schema(description = "评审操作人编号", example = "1001")
|
||||||
|
private Long operatorId;
|
||||||
|
|
||||||
|
@Schema(description = "评审结论:0 通过,1 不通过", example = "1")
|
||||||
|
private Integer conclusion;
|
||||||
|
|
||||||
|
@Schema(description = "评审内容,支持富文本")
|
||||||
|
private String reviewContent;
|
||||||
|
|
||||||
|
@Schema(description = "需求预估工时", example = "16.5")
|
||||||
|
private BigDecimal requirementEstimatedHours;
|
||||||
|
|
||||||
|
@Schema(description = "参会人列表")
|
||||||
|
private List<RequirementReviewAttendeeItem> attendees;
|
||||||
|
|
||||||
|
@Schema(description = "会议资料附件")
|
||||||
|
private List<AttachmentItem> attachments;
|
||||||
|
|
||||||
|
@Schema(description = "实际评审日期", example = "2026-05-20")
|
||||||
|
private LocalDate reviewTime;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.review.vo;
|
||||||
|
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.review.RequirementReviewAttendeeItem;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 需求评审提交 Request VO")
|
||||||
|
@Data
|
||||||
|
public class RequirementReviewSubmitReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "产品编号,产品需求评审时必填", example = "1024")
|
||||||
|
private Long productId;
|
||||||
|
|
||||||
|
@Schema(description = "项目编号,项目需求评审时必填", example = "2048")
|
||||||
|
private Long projectId;
|
||||||
|
|
||||||
|
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
|
||||||
|
@NotNull(message = "需求编号不能为空")
|
||||||
|
private Long requirementId;
|
||||||
|
|
||||||
|
@Schema(description = "评审操作人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||||
|
@NotNull(message = "评审操作人不能为空")
|
||||||
|
private Long operatorId;
|
||||||
|
|
||||||
|
@Schema(description = "评审结论:0 通过,1 不通过", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
@NotNull(message = "评审结论不能为空")
|
||||||
|
@Min(value = 0, message = "评审结论不合法")
|
||||||
|
@Max(value = 1, message = "评审结论不合法")
|
||||||
|
private Integer conclusion;
|
||||||
|
|
||||||
|
@Schema(description = "评审内容,支持富文本")
|
||||||
|
private String reviewContent;
|
||||||
|
|
||||||
|
@Schema(description = "需求预估工时", example = "16.5")
|
||||||
|
private BigDecimal requirementEstimatedHours;
|
||||||
|
|
||||||
|
@Schema(description = "参会人列表")
|
||||||
|
private List<RequirementReviewAttendeeItem> attendees;
|
||||||
|
|
||||||
|
@Schema(description = "会议资料附件")
|
||||||
|
private List<AttachmentItem> attachments;
|
||||||
|
|
||||||
|
@Schema(description = "实际评审日期", example = "2026-05-20")
|
||||||
|
private LocalDate reviewTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.dataobject.overtime;
|
||||||
|
|
||||||
|
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.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班申请表。
|
||||||
|
*/
|
||||||
|
@TableName("rdms_overtime_application")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OvertimeApplicationDO extends BaseDO {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long applicantId;
|
||||||
|
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
private LocalDate overtimeDate;
|
||||||
|
|
||||||
|
private String overtimeDuration;
|
||||||
|
|
||||||
|
private String overtimeReason;
|
||||||
|
|
||||||
|
private String overtimeContent;
|
||||||
|
|
||||||
|
private Long approverId;
|
||||||
|
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
private String approvalComment;
|
||||||
|
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
private LocalDateTime approvalTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.dataobject.overtime;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班申请状态日志表。
|
||||||
|
*/
|
||||||
|
@TableName("rdms_overtime_application_status_log")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OvertimeApplicationStatusLogDO extends BaseDO {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long applicationId;
|
||||||
|
|
||||||
|
private String actionType;
|
||||||
|
|
||||||
|
private String fromStatus;
|
||||||
|
|
||||||
|
private String toStatus;
|
||||||
|
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
private Long operatorUserId;
|
||||||
|
|
||||||
|
private String operatorName;
|
||||||
|
|
||||||
|
private String applicantNameSnapshot;
|
||||||
|
|
||||||
|
private LocalDate overtimeDateSnapshot;
|
||||||
|
|
||||||
|
private String overtimeDurationSnapshot;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ public class ProductRequirementDO extends BaseDO {
|
|||||||
*/
|
*/
|
||||||
private String currentHandlerUserNickname;
|
private String currentHandlerUserNickname;
|
||||||
/**
|
/**
|
||||||
* 默认实现项目ID,分流后可回填
|
* 默认实现项目ID,指派后可回填
|
||||||
*/
|
*/
|
||||||
private Long implementProjectId;
|
private Long implementProjectId;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.njcn.rdms.module.project.dal.dataobject.product;
|
package com.njcn.rdms.module.project.dal.dataobject.product;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
@@ -51,9 +52,4 @@ public class ProductRequirementStatusLogDO extends BaseDO {
|
|||||||
* 需求标题快照
|
* 需求标题快照
|
||||||
*/
|
*/
|
||||||
private String requirementTitleSnapshot;
|
private String requirementTitleSnapshot;
|
||||||
/**
|
|
||||||
* 备注
|
|
||||||
*/
|
|
||||||
private String remark;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.dataobject.review;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求评审参会人快照。
|
||||||
|
*/
|
||||||
|
@Schema(description = "需求评审参会人快照")
|
||||||
|
@Data
|
||||||
|
public class RequirementReviewAttendeeItem {
|
||||||
|
|
||||||
|
@Schema(description = "用户编号", example = "1024")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "用户昵称", example = "张三")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.dataobject.review;
|
||||||
|
|
||||||
|
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.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求评审记录。
|
||||||
|
*/
|
||||||
|
@TableName(value = "rdms_requirement_review", autoResultMap = true)
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class RequirementReviewDO extends BaseDO {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String objectType;
|
||||||
|
|
||||||
|
private Long requirementId;
|
||||||
|
|
||||||
|
private Long operatorId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评审结论:0 通过,1 不通过。
|
||||||
|
*/
|
||||||
|
private Integer conclusion;
|
||||||
|
|
||||||
|
private String reviewContent;
|
||||||
|
|
||||||
|
private BigDecimal requirementEstimatedHours;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private List<RequirementReviewAttendeeItem> attendees;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private List<AttachmentItem> attachments;
|
||||||
|
|
||||||
|
private LocalDateTime reviewTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -111,4 +111,18 @@ public interface UserObjectRoleMapper extends BaseMapperX<UserObjectRoleDO> {
|
|||||||
.eq(UserObjectRoleDO::getStatus, 0));
|
.eq(UserObjectRoleDO::getStatus, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台「我负责的项目」:批量查一批对象下的活跃成员角色行(status=0)。
|
||||||
|
* 一次拿全,内存按 objectId 分组,避免逐项目 N+1。
|
||||||
|
*/
|
||||||
|
default List<UserObjectRoleDO> selectActiveListByObjectTypeAndObjectIds(String objectType, Collection<Long> objectIds) {
|
||||||
|
if (objectIds == null || objectIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return selectList(new LambdaQueryWrapperX<UserObjectRoleDO>()
|
||||||
|
.eq(UserObjectRoleDO::getObjectType, objectType)
|
||||||
|
.in(UserObjectRoleDO::getObjectId, objectIds)
|
||||||
|
.eq(UserObjectRoleDO::getStatus, 0));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.mysql.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface OvertimeApplicationMapper extends BaseMapperX<OvertimeApplicationDO> {
|
||||||
|
|
||||||
|
default PageResult<OvertimeApplicationDO> selectMyPage(Long applicantId, OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = buildPageQuery(reqVO);
|
||||||
|
queryWrapper.eq(OvertimeApplicationDO::getApplicantId, applicantId);
|
||||||
|
queryWrapper.orderByDesc(OvertimeApplicationDO::getSubmitTime)
|
||||||
|
.orderByDesc(OvertimeApplicationDO::getId);
|
||||||
|
return selectPage(reqVO, queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
default PageResult<OvertimeApplicationDO> selectApprovalPage(Long approverId, OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = buildPageQuery(reqVO);
|
||||||
|
queryWrapper.eq(OvertimeApplicationDO::getApproverId, approverId);
|
||||||
|
queryWrapper.orderByDesc(OvertimeApplicationDO::getSubmitTime)
|
||||||
|
.orderByDesc(OvertimeApplicationDO::getId);
|
||||||
|
return selectPage(reqVO, queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
default OvertimeApplicationDO selectByIdAndApplicantId(Long id, Long applicantId) {
|
||||||
|
return selectOne(new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||||
|
.eq(OvertimeApplicationDO::getId, id)
|
||||||
|
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
default int updateByIdAndStatus(OvertimeApplicationDO update, Long id, String fromStatus) {
|
||||||
|
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||||
|
.eq(OvertimeApplicationDO::getId, id)
|
||||||
|
.eq(OvertimeApplicationDO::getStatusCode, fromStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
default int updateByIdAndStatusAndApplicantId(OvertimeApplicationDO update, Long id, String fromStatus,
|
||||||
|
Long applicantId) {
|
||||||
|
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||||
|
.eq(OvertimeApplicationDO::getId, id)
|
||||||
|
.eq(OvertimeApplicationDO::getStatusCode, fromStatus)
|
||||||
|
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
default int updateByIdAndStatusesAndApplicantId(OvertimeApplicationDO update, Long id,
|
||||||
|
Collection<String> fromStatuses, Long applicantId) {
|
||||||
|
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||||
|
.eq(OvertimeApplicationDO::getId, id)
|
||||||
|
.in(OvertimeApplicationDO::getStatusCode, fromStatuses)
|
||||||
|
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LambdaQueryWrapperX<OvertimeApplicationDO> buildPageQuery(OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = new LambdaQueryWrapperX<>();
|
||||||
|
queryWrapper.likeIfPresent(OvertimeApplicationDO::getApplicantName, reqVO.getApplicantName())
|
||||||
|
.eqIfPresent(OvertimeApplicationDO::getApproverId, reqVO.getApproverId())
|
||||||
|
.likeIfPresent(OvertimeApplicationDO::getApproverName, reqVO.getApproverName())
|
||||||
|
.eqIfPresent(OvertimeApplicationDO::getStatusCode, reqVO.getStatusCode())
|
||||||
|
.betweenIfPresent(OvertimeApplicationDO::getOvertimeDate, reqVO.getOvertimeDate())
|
||||||
|
.betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime());
|
||||||
|
if (StringUtils.hasText(reqVO.getKeyword())) {
|
||||||
|
queryWrapper.and(wrapper -> wrapper.like(OvertimeApplicationDO::getOvertimeReason, reqVO.getKeyword())
|
||||||
|
.or()
|
||||||
|
.like(OvertimeApplicationDO::getOvertimeContent, reqVO.getKeyword()));
|
||||||
|
}
|
||||||
|
return queryWrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.mysql.overtime;
|
||||||
|
|
||||||
|
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.overtime.OvertimeApplicationStatusLogDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface OvertimeApplicationStatusLogMapper extends BaseMapperX<OvertimeApplicationStatusLogDO> {
|
||||||
|
|
||||||
|
default List<OvertimeApplicationStatusLogDO> selectListByApplicationId(Long applicationId) {
|
||||||
|
return selectList(new LambdaQueryWrapperX<OvertimeApplicationStatusLogDO>()
|
||||||
|
.eq(OvertimeApplicationStatusLogDO::getApplicationId, applicationId)
|
||||||
|
.orderByDesc(OvertimeApplicationStatusLogDO::getCreateTime)
|
||||||
|
.orderByDesc(OvertimeApplicationStatusLogDO::getId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
|||||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
|
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,4 +23,20 @@ public interface ProductRequirementStatusLogMapper extends BaseMapperX<ProductRe
|
|||||||
.orderByDesc(ProductRequirementStatusLogDO::getCreateTime));
|
.orderByDesc(ProductRequirementStatusLogDO::getCreateTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据需求ID集合和目标状态集合查询状态变更日志。
|
||||||
|
*/
|
||||||
|
default List<ProductRequirementStatusLogDO> selectListByRequirementIdsAndToStatuses(List<Long> requirementIds,
|
||||||
|
List<String> toStatuses) {
|
||||||
|
if (requirementIds == null || requirementIds.isEmpty()
|
||||||
|
|| toStatuses == null || toStatuses.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return selectList(new LambdaQueryWrapperX<ProductRequirementStatusLogDO>()
|
||||||
|
.in(ProductRequirementStatusLogDO::getRequirementId, requirementIds)
|
||||||
|
.in(ProductRequirementStatusLogDO::getToStatus, toStatuses)
|
||||||
|
.orderByDesc(ProductRequirementStatusLogDO::getCreateTime)
|
||||||
|
.orderByDesc(ProductRequirementStatusLogDO::getId));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,4 +73,14 @@ public interface ProjectMapper extends BaseMapperX<ProjectDO> {
|
|||||||
.eq(ProjectDO::getStatusCode, statusCode));
|
.eq(ProjectDO::getStatusCode, statusCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅更新项目 progressRate,不动其他字段(避免污染 lastStatusReason 等)。
|
||||||
|
*/
|
||||||
|
default int updateProgressRateById(Long id, java.math.BigDecimal progressRate) {
|
||||||
|
ProjectDO update = new ProjectDO();
|
||||||
|
update.setProgressRate(progressRate);
|
||||||
|
return update(update, new LambdaQueryWrapperX<ProjectDO>()
|
||||||
|
.eq(ProjectDO::getId, id));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
|||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -50,23 +48,6 @@ public interface ExecutionAssigneeMapper extends BaseMapperX<ExecutionAssigneeDO
|
|||||||
.isNull(ExecutionAssigneeDO::getRemovedAt));
|
.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<Long> selectActiveExecutionIdsByProjectIdAndUserId(@Param("projectId") Long projectId,
|
|
||||||
@Param("userId") Long userId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 execution_id 批量软删执行协办(含已 removed 的历史段)。
|
* 按 execution_id 批量软删执行协办(含已 removed 的历史段)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.njcn.rdms.module.project.dal.mysql.project.execution;
|
package com.njcn.rdms.module.project.dal.mysql.project.execution;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
@@ -7,12 +9,11 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
|||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -33,44 +34,97 @@ public interface ProjectExecutionMapper extends BaseMapperX<ProjectExecutionDO>
|
|||||||
}
|
}
|
||||||
|
|
||||||
default PageResult<ProjectExecutionDO> selectPageByProjectId(Long projectId,
|
default PageResult<ProjectExecutionDO> selectPageByProjectId(Long projectId,
|
||||||
VisibilityScope scope,
|
ProjectExecutionPageReqVO reqVO,
|
||||||
ProjectExecutionPageReqVO reqVO) {
|
List<String> terminalStatusCodes,
|
||||||
// 可见性短路:非 seesAll 且无任何可见执行 → 空页,避免后续 IN () SQL
|
LocalDate today,
|
||||||
if (!scope.seesAll() && scope.executionIds().isEmpty()) {
|
LocalDate weekStart,
|
||||||
return PageResult.empty();
|
LocalDate weekEnd) {
|
||||||
}
|
Page<ProjectExecutionDO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
|
||||||
LambdaQueryWrapperX<ProjectExecutionDO> queryWrapper = new LambdaQueryWrapperX<>();
|
IPage<ProjectExecutionDO> ipage = doSelectPageByProjectId(
|
||||||
queryWrapper.eq(ProjectExecutionDO::getProjectId, projectId);
|
projectId, reqVO, terminalStatusCodes, today, weekStart, weekEnd, page);
|
||||||
queryWrapper.eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType());
|
return new PageResult<>(ipage.getRecords(), ipage.getTotal());
|
||||||
queryWrapper.eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId());
|
|
||||||
queryWrapper.eqIfPresent(ProjectExecutionDO::getStatusCode, reqVO.getStatusCode());
|
|
||||||
queryWrapper.eqIfPresent(ProjectExecutionDO::getPriority, reqVO.getPriority());
|
|
||||||
queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
|
||||||
queryWrapper.orderByAsc(ProjectExecutionDO::getPriority);
|
|
||||||
queryWrapper.orderByDesc(BaseDO::getCreateTime);
|
|
||||||
queryWrapper.orderByDesc(ProjectExecutionDO::getId);
|
|
||||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword()));
|
|
||||||
}
|
|
||||||
if (!scope.seesAll()) {
|
|
||||||
queryWrapper.in(ProjectExecutionDO::getId, scope.executionIds());
|
|
||||||
}
|
|
||||||
return selectPage(reqVO, queryWrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查 userId 在指定项目下,作为 owner 的所有执行 ID。
|
* 项目下执行分页查询。
|
||||||
* 用于 VisibilityScopeResolver 收集"我是执行负责人"的 scope 来源。
|
* <p>SQL 用 @Select 直接控制,主表以别名 t 暴露,EXISTS 子查询用 t.id 关联;
|
||||||
|
* 避免 LambdaWrapper + .apply 嵌入裸 SQL 时依赖 "MyBatis-Plus 不给主表加别名" 这一实现细节。
|
||||||
|
* 与任务侧 {@code ProjectTaskMapper.selectAggregatePageByProjectId} 同款风格。
|
||||||
|
*
|
||||||
|
* <p>involveUserId 非空时附加 (owner_id = involveUserId OR 活跃协办含 involveUserId);
|
||||||
|
* 否则该过滤分支跳过("看项目下全部")。
|
||||||
*/
|
*/
|
||||||
default List<Long> selectIdsByProjectIdAndOwnerId(Long projectId, Long userId) {
|
@Select("""
|
||||||
return selectList(new LambdaQueryWrapperX<ProjectExecutionDO>()
|
<script>
|
||||||
.select(ProjectExecutionDO::getId)
|
SELECT t.*
|
||||||
.eq(ProjectExecutionDO::getProjectId, projectId)
|
FROM rdms_project_execution t
|
||||||
.eq(ProjectExecutionDO::getOwnerId, userId))
|
<where>
|
||||||
.stream()
|
t.deleted = b'0'
|
||||||
.map(ProjectExecutionDO::getId)
|
AND t.project_id = #{projectId}
|
||||||
.toList();
|
<if test="reqVO.executionType != null and reqVO.executionType != ''">
|
||||||
}
|
AND t.execution_type = #{reqVO.executionType}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.statusCode != null and reqVO.statusCode != ''">
|
||||||
|
AND t.status_code = #{reqVO.statusCode}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.execution_name LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_execution_assignee a
|
||||||
|
WHERE a.execution_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<!-- 截止时间范围 chip:基于 planned_end_date,三个桶均排除终态(对齐任务 summary 口径) -->
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY t.priority ASC, t.create_time DESC, t.id DESC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
IPage<ProjectExecutionDO> doSelectPageByProjectId(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("reqVO") ProjectExecutionPageReqVO reqVO,
|
||||||
|
@Param("terminalStatusCodes") List<String> terminalStatusCodes,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd,
|
||||||
|
Page<ProjectExecutionDO> page);
|
||||||
|
|
||||||
default List<ProjectExecutionDO> selectListByOwnerId(Long ownerId) {
|
default List<ProjectExecutionDO> selectListByOwnerId(Long ownerId) {
|
||||||
return selectList(new LambdaQueryWrapperX<ProjectExecutionDO>()
|
return selectList(new LambdaQueryWrapperX<ProjectExecutionDO>()
|
||||||
@@ -90,28 +144,86 @@ public interface ProjectExecutionMapper extends BaseMapperX<ProjectExecutionDO>
|
|||||||
}
|
}
|
||||||
|
|
||||||
default Integer countByProjectIdAndStatusCode(Long projectId,
|
default Integer countByProjectIdAndStatusCode(Long projectId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectExecutionStatusBoardReqVO reqVO,
|
ProjectExecutionStatusBoardReqVO reqVO,
|
||||||
String statusCode) {
|
String statusCode,
|
||||||
// 可见性短路:非 seesAll 且无任何可见执行 → 计数 0
|
List<String> terminalStatusCodes,
|
||||||
if (!scope.seesAll() && scope.executionIds().isEmpty()) {
|
LocalDate today,
|
||||||
return 0;
|
LocalDate weekStart,
|
||||||
}
|
LocalDate weekEnd) {
|
||||||
LambdaQueryWrapperX<ProjectExecutionDO> queryWrapper = new LambdaQueryWrapperX<ProjectExecutionDO>()
|
return Math.toIntExact(doCountByProjectIdAndStatusCode(
|
||||||
.eq(ProjectExecutionDO::getProjectId, projectId)
|
projectId, reqVO, statusCode, terminalStatusCodes, today, weekStart, weekEnd));
|
||||||
.eq(ProjectExecutionDO::getStatusCode, statusCode)
|
|
||||||
.eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType())
|
|
||||||
.eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId())
|
|
||||||
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
|
||||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword()));
|
|
||||||
}
|
|
||||||
if (!scope.seesAll()) {
|
|
||||||
queryWrapper.in(ProjectExecutionDO::getId, scope.executionIds());
|
|
||||||
}
|
|
||||||
return Math.toIntExact(selectCount(queryWrapper));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目下指定状态的执行计数(与 doSelectPageByProjectId 同款过滤口径)。
|
||||||
|
* 同上:用 @Select 显式表别名 t 替代 LambdaWrapper + .apply EXISTS 写法。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM rdms_project_execution t
|
||||||
|
<where>
|
||||||
|
t.deleted = b'0'
|
||||||
|
AND t.project_id = #{projectId}
|
||||||
|
AND t.status_code = #{statusCode}
|
||||||
|
<if test="reqVO.executionType != null and reqVO.executionType != ''">
|
||||||
|
AND t.execution_type = #{reqVO.executionType}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.execution_name LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_execution_assignee a
|
||||||
|
WHERE a.execution_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<!-- 截止时间范围 chip:基于 planned_end_date,三个桶均排除终态(对齐任务 summary 口径) -->
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Long doCountByProjectIdAndStatusCode(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("reqVO") ProjectExecutionStatusBoardReqVO reqVO,
|
||||||
|
@Param("statusCode") String statusCode,
|
||||||
|
@Param("terminalStatusCodes") List<String> terminalStatusCodes,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TD-016:按 projectRequirementIds 批量聚合承接执行的平均进度,避免列表 N+1。
|
* TD-016:按 projectRequirementIds 批量聚合承接执行的平均进度,避免列表 N+1。
|
||||||
* <p>口径:仅统计 deleted = 0 + status_code NOT IN(excludedStatusCodes) 的执行;
|
* <p>口径:仅统计 deleted = 0 + status_code NOT IN(excludedStatusCodes) 的执行;
|
||||||
@@ -165,4 +277,27 @@ public interface ProjectExecutionMapper extends BaseMapperX<ProjectExecutionDO>
|
|||||||
.eq(ProjectExecutionDO::getStatusCode, fromStatus));
|
.eq(ProjectExecutionDO::getStatusCode, fromStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口二:一批项目下的进行中执行数(按 project_id 分组,排除终态)。
|
||||||
|
* 返回 Map:projectId(Long) / executionCount(Long)。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT project_id AS projectId,
|
||||||
|
CAST(COUNT(*) AS SIGNED) AS executionCount
|
||||||
|
FROM rdms_project_execution
|
||||||
|
WHERE deleted = b'0'
|
||||||
|
AND project_id IN
|
||||||
|
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
GROUP BY project_id
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> selectExecutionCountGroupByProjectIds(
|
||||||
|
@Param("projectIds") Collection<Long> projectIds,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package com.njcn.rdms.module.project.dal.mysql.project.task;
|
package com.njcn.rdms.module.project.dal.mysql.project.task;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
|
||||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
import lombok.Data;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -31,33 +33,68 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default PageResult<ProjectTaskDO> selectPageByExecutionId(Long projectId, Long executionId,
|
default PageResult<ProjectTaskDO> selectPageByExecutionId(Long projectId, Long executionId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectTaskPageReqVO reqVO) {
|
ProjectTaskPageReqVO reqVO) {
|
||||||
// 可见性短路:非 seesAll 且无任何可见任务 → 空页
|
Page<ProjectTaskDO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
|
||||||
if (!scope.seesAll() && scope.taskIds().isEmpty()) {
|
IPage<ProjectTaskDO> ipage = doSelectPageByExecutionId(projectId, executionId, reqVO, page);
|
||||||
return PageResult.empty();
|
return new PageResult<>(ipage.getRecords(), ipage.getTotal());
|
||||||
}
|
|
||||||
LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<>();
|
|
||||||
queryWrapper.eq(ProjectTaskDO::getProjectId, projectId);
|
|
||||||
queryWrapper.eq(ProjectTaskDO::getExecutionId, executionId);
|
|
||||||
queryWrapper.eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId());
|
|
||||||
queryWrapper.eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId());
|
|
||||||
queryWrapper.eqIfPresent(ProjectTaskDO::getStatusCode, reqVO.getStatusCode());
|
|
||||||
queryWrapper.eqIfPresent(ProjectTaskDO::getPriority, reqVO.getPriority());
|
|
||||||
queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
|
||||||
queryWrapper.orderByAsc(ProjectTaskDO::getParentTaskId);
|
|
||||||
queryWrapper.orderByAsc(ProjectTaskDO::getPriority);
|
|
||||||
queryWrapper.orderByDesc(BaseDO::getCreateTime);
|
|
||||||
queryWrapper.orderByDesc(ProjectTaskDO::getId);
|
|
||||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword()));
|
|
||||||
}
|
|
||||||
if (!scope.seesAll()) {
|
|
||||||
queryWrapper.in(ProjectTaskDO::getId, scope.taskIds());
|
|
||||||
}
|
|
||||||
return selectPage(reqVO, queryWrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行内任务分页查询。
|
||||||
|
* <p>SQL 用 @Select 显式表别名 t,EXISTS 子查询用 t.id 关联 rdms_task_assignee;
|
||||||
|
* 与项目级 aggregate page 同款风格。
|
||||||
|
*
|
||||||
|
* <p>involveUserId 非空时附加 (owner_id = involveUserId OR 活跃协办含 involveUserId);
|
||||||
|
* 与 ownerId 文档标注「二选一」由前端保证(不做后端互斥校验)。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT t.*
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.deleted = b'0'
|
||||||
|
AND t.project_id = #{projectId}
|
||||||
|
AND t.execution_id = #{executionId}
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.statusCode != null and reqVO.statusCode != ''">
|
||||||
|
AND t.status_code = #{reqVO.statusCode}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY t.parent_task_id ASC, t.priority ASC, t.create_time DESC, t.id DESC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
IPage<ProjectTaskDO> doSelectPageByExecutionId(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("executionId") Long executionId,
|
||||||
|
@Param("reqVO") ProjectTaskPageReqVO reqVO,
|
||||||
|
Page<ProjectTaskDO> page);
|
||||||
|
|
||||||
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
|
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
|
||||||
ProjectTaskDO update = new ProjectTaskDO();
|
ProjectTaskDO update = new ProjectTaskDO();
|
||||||
update.setStatusCode(toStatus);
|
update.setStatusCode(toStatus);
|
||||||
@@ -186,6 +223,26 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
@Param("executionIds") Collection<Long> executionIds,
|
@Param("executionIds") Collection<Long> executionIds,
|
||||||
@Param("excludedStatusCodes") Collection<String> excludedStatusCodes);
|
@Param("excludedStatusCodes") Collection<String> excludedStatusCodes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目进度推算:跨执行聚合,按项目下所有根任务 progressRate 简单平均;无根任务时 SQL 返回 null。
|
||||||
|
* 与执行口径一致(parent_task_id IS NULL + excludedStatusCodes),区别仅是不限定 execution_id。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT AVG(COALESCE(progress_rate, 0))
|
||||||
|
FROM rdms_task
|
||||||
|
WHERE deleted = b'0'
|
||||||
|
AND project_id = #{projectId}
|
||||||
|
AND parent_task_id IS NULL
|
||||||
|
<if test="excludedStatusCodes != null and excludedStatusCodes.size() > 0">
|
||||||
|
AND status_code NOT IN
|
||||||
|
<foreach collection="excludedStatusCodes" item="statusCode" open="(" separator="," close=")">#{statusCode}</foreach>
|
||||||
|
</if>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
BigDecimal selectRootTaskAvgProgressByProjectId(@Param("projectId") Long projectId,
|
||||||
|
@Param("excludedStatusCodes") Collection<String> excludedStatusCodes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行详情完成态聚合:返回根任务总数与"已完成"数,用于 execution complete 按钮判定。
|
* 执行详情完成态聚合:返回根任务总数与"已完成"数,用于 execution complete 按钮判定。
|
||||||
* 与 selectRootTaskAvgProgressByExecutionId 共用同一根任务筛选口径(execution_id + parent_task_id IS NULL + excludedStatusCodes)。
|
* 与 selectRootTaskAvgProgressByExecutionId 共用同一根任务筛选口径(execution_id + parent_task_id IS NULL + excludedStatusCodes)。
|
||||||
@@ -249,78 +306,57 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
.eq(ProjectTaskDO::getId, id));
|
.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<Long> 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<Long> selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId(
|
|
||||||
@Param("projectId") Long projectId,
|
|
||||||
@Param("executionId") Long executionId,
|
|
||||||
@Param("userId") Long userId);
|
|
||||||
|
|
||||||
default Integer countByProjectIdAndExecutionIdAndStatusCode(Long projectId, Long executionId,
|
default Integer countByProjectIdAndExecutionIdAndStatusCode(Long projectId, Long executionId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectTaskStatusBoardReqVO reqVO,
|
ProjectTaskStatusBoardReqVO reqVO,
|
||||||
String statusCode) {
|
String statusCode) {
|
||||||
// 可见性短路:非 seesAll 且无任何可见任务 → 0
|
return Math.toIntExact(doCountByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, reqVO, statusCode));
|
||||||
if (!scope.seesAll() && scope.taskIds().isEmpty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<ProjectTaskDO>()
|
|
||||||
.eq(ProjectTaskDO::getProjectId, projectId)
|
|
||||||
.eq(ProjectTaskDO::getExecutionId, executionId)
|
|
||||||
.eq(ProjectTaskDO::getStatusCode, statusCode)
|
|
||||||
.eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId())
|
|
||||||
.eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId())
|
|
||||||
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
|
||||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword()));
|
|
||||||
}
|
|
||||||
if (!scope.seesAll()) {
|
|
||||||
queryWrapper.in(ProjectTaskDO::getId, scope.taskIds());
|
|
||||||
}
|
|
||||||
return Math.toIntExact(selectCount(queryWrapper));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行内任务按状态计数(与 doSelectPageByExecutionId 同款过滤口径,含 involveUserId 协办分支)。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.deleted = b'0'
|
||||||
|
AND t.project_id = #{projectId}
|
||||||
|
AND t.execution_id = #{executionId}
|
||||||
|
AND t.status_code = #{statusCode}
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Long doCountByProjectIdAndExecutionIdAndStatusCode(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("executionId") Long executionId,
|
||||||
|
@Param("reqVO") ProjectTaskStatusBoardReqVO reqVO,
|
||||||
|
@Param("statusCode") String statusCode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 收集执行下的所有任务 id(含子孙——子孙必然同 execution_id,所以一把抓即可)。
|
* 收集执行下的所有任务 id(含子孙——子孙必然同 execution_id,所以一把抓即可)。
|
||||||
* 用于"删除执行"时的级联软删。
|
* 用于"删除执行"时的级联软删。
|
||||||
@@ -339,7 +375,7 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 从给定任务出发,递归向下收集自身 + 所有子孙任务 id(递归 CTE)。
|
* 从给定任务出发,递归向下收集自身 + 所有子孙任务 id(递归 CTE)。
|
||||||
* 用于"删除任务"时的级联软删。复用与 selectOwnedTaskAndDescendantIdsByProjectIdAndUserId 同款 CTE 模式。
|
* 用于"删除任务"时的级联软删。
|
||||||
*
|
*
|
||||||
* 任务表已逻辑删除的行不参与递归。
|
* 任务表已逻辑删除的行不参与递归。
|
||||||
*/
|
*/
|
||||||
@@ -379,4 +415,368 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
.eq(ProjectTaskDO::getExecutionId, executionId)));
|
.eq(ProjectTaskDO::getExecutionId, executionId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== 项目级跨执行聚合查询 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目级跨执行任务分页查询。
|
||||||
|
*
|
||||||
|
* 语义:
|
||||||
|
* - involveUserId 不为 null → 附加 (t.owner_id = ? OR exists active assignee user_id = ?)
|
||||||
|
* - statusCodes 非空 → t.status_code IN (...)
|
||||||
|
* - dueRange='overdue' 且 terminalStatusCodes 非空 → 排除终态
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT t.*
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionIds != null and !reqVO.executionIds.isEmpty()">
|
||||||
|
AND t.execution_id IN
|
||||||
|
<foreach collection="reqVO.executionIds" item="eid" open="(" separator="," close=")">#{eid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionStatusCodes != null and !reqVO.executionStatusCodes.isEmpty()">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND e.status_code IN
|
||||||
|
<foreach collection="reqVO.executionStatusCodes" item="esc" open="(" separator="," close=")">#{esc}</foreach>
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionInvolveUserId != null">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND (
|
||||||
|
e.owner_id = #{reqVO.executionInvolveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_execution_assignee ea
|
||||||
|
WHERE ea.execution_id = e.id
|
||||||
|
AND ea.user_id = #{reqVO.executionInvolveUserId}
|
||||||
|
AND ea.removed_at IS NULL
|
||||||
|
AND ea.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.statusCodes != null and !reqVO.statusCodes.isEmpty()">
|
||||||
|
AND t.status_code IN
|
||||||
|
<foreach collection="reqVO.statusCodes" item="sc" open="(" separator="," close=")">#{sc}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
<choose>
|
||||||
|
<when test="reqVO.sortBy == 'priority'">ORDER BY t.priority</when>
|
||||||
|
<when test="reqVO.sortBy == 'updateTime'">ORDER BY t.update_time</when>
|
||||||
|
<when test="reqVO.sortBy == 'createTime'">ORDER BY t.create_time</when>
|
||||||
|
<otherwise>ORDER BY t.planned_end_date</otherwise>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<when test="reqVO.sortOrder == 'desc'">DESC</when>
|
||||||
|
<otherwise>ASC</otherwise>
|
||||||
|
</choose>
|
||||||
|
, t.id DESC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
IPage<ProjectTaskDO> selectAggregatePageByProjectId(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("reqVO") ProjectTaskAggregatePageReqVO reqVO,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
Page<ProjectTaskDO> page);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目级跨执行任务按状态分组计数(status-board)。
|
||||||
|
* 入参同 page 但去除分页 / sort / statusCodes 筛选。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT t.status_code AS statusCode, COUNT(*) AS count
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionIds != null and !reqVO.executionIds.isEmpty()">
|
||||||
|
AND t.execution_id IN
|
||||||
|
<foreach collection="reqVO.executionIds" item="eid" open="(" separator="," close=")">#{eid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionStatusCodes != null and !reqVO.executionStatusCodes.isEmpty()">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND e.status_code IN
|
||||||
|
<foreach collection="reqVO.executionStatusCodes" item="esc" open="(" separator="," close=")">#{esc}</foreach>
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionInvolveUserId != null">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND (
|
||||||
|
e.owner_id = #{reqVO.executionInvolveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_execution_assignee ea
|
||||||
|
WHERE ea.execution_id = e.id
|
||||||
|
AND ea.user_id = #{reqVO.executionInvolveUserId}
|
||||||
|
AND ea.removed_at IS NULL
|
||||||
|
AND ea.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
GROUP BY t.status_code
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<StatusCountRow> selectAggregateStatusCount(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("reqVO") ProjectTaskAggregateStatusBoardReqVO reqVO,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd,
|
||||||
|
@Param("today") LocalDate today);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* summary 的 4 个数字一次查出来,避免 4 次扫表。
|
||||||
|
*
|
||||||
|
* 返回 Map 结构:
|
||||||
|
* overdue → Long
|
||||||
|
* dueToday → Long
|
||||||
|
* dueThisWeek → Long
|
||||||
|
* doneThisWeek → Long
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS overdue,
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date = #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS dueToday,
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS dueThisWeek,
|
||||||
|
CAST(SUM(CASE WHEN t.actual_end_date BETWEEN #{weekStart} AND #{today}
|
||||||
|
AND t.status_code = #{completedStatusCode}
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS doneThisWeek
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Map<String, Long> selectAggregateSummaryCounts(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("involveUserId") Long involveUserId,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("completedStatusCode") String completedStatusCode,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* status-board 计数行 — 内嵌静态类,与 mapper 共享生命周期。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
class StatusCountRow {
|
||||||
|
private String statusCode;
|
||||||
|
private Long count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 工作台「我的项目」聚合计数 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口一:当前用户(owner_id)在一批项目下的任务总数与未完成数(按 project_id 分组)。
|
||||||
|
* totalCount=全部我负责任务;pendingCount=状态非终态(终态集为空则等于 totalCount)。
|
||||||
|
* 返回 Map:projectId(Long) / totalCount(Long) / pendingCount(Long)。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT project_id AS projectId,
|
||||||
|
CAST(COUNT(*) AS SIGNED) AS totalCount,
|
||||||
|
CAST(SUM(CASE WHEN 1 = 1
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS pendingCount
|
||||||
|
FROM rdms_task
|
||||||
|
WHERE deleted = b'0'
|
||||||
|
AND owner_id = #{ownerId}
|
||||||
|
AND project_id IN
|
||||||
|
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
|
||||||
|
GROUP BY project_id
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> selectMyTaskCountGroupByProjectIds(
|
||||||
|
@Param("ownerId") Long ownerId,
|
||||||
|
@Param("projectIds") Collection<Long> projectIds,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口二:一批项目下的进行中任务数与逾期任务数(按 project_id 分组,一次扫表出两数)。
|
||||||
|
* taskCount=状态非终态;overdueCount=planned_end_date < today 且状态非终态。
|
||||||
|
* 返回 Map:projectId(Long) / taskCount(Long) / overdueCount(Long)。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT project_id AS projectId,
|
||||||
|
CAST(SUM(CASE WHEN 1 = 1
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS taskCount,
|
||||||
|
CAST(SUM(CASE WHEN planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS overdueCount
|
||||||
|
FROM rdms_task
|
||||||
|
WHERE deleted = b'0'
|
||||||
|
AND project_id IN
|
||||||
|
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
|
||||||
|
GROUP BY project_id
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> selectTaskAndOverdueCountGroupByProjectIds(
|
||||||
|
@Param("projectIds") Collection<Long> projectIds,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("today") LocalDate today);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口二 members:一批项目下每个负责人(owner_id)的进行中任务数(按 project_id, owner_id 分组)。
|
||||||
|
* 排除 owner_id 为空的任务。返回 Map:projectId(Long) / ownerId(Long) / activeTaskCount(Long)。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT project_id AS projectId,
|
||||||
|
owner_id AS ownerId,
|
||||||
|
CAST(COUNT(*) AS SIGNED) AS activeTaskCount
|
||||||
|
FROM rdms_task
|
||||||
|
WHERE deleted = b'0'
|
||||||
|
AND owner_id IS NOT NULL
|
||||||
|
AND project_id IN
|
||||||
|
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
GROUP BY project_id, owner_id
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> selectActiveTaskCountGroupByProjectIdAndOwner(
|
||||||
|
@Param("projectIds") Collection<Long> projectIds,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
|||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
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.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -50,41 +48,6 @@ public interface TaskAssigneeMapper extends BaseMapperX<TaskAssigneeDO> {
|
|||||||
.orderByAsc(TaskAssigneeDO::getId));
|
.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<Long> 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<Long> selectActiveTaskIdsByProjectIdAndExecutionIdAndUserId(
|
|
||||||
@Param("projectId") Long projectId,
|
|
||||||
@Param("executionId") Long executionId,
|
|
||||||
@Param("userId") Long userId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按主键 + 任务 ID 双键查;返回的记录可能已失效(removed_at != null),由调用方判断。
|
* 按主键 + 任务 ID 双键查;返回的记录可能已失效(removed_at != null),由调用方判断。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.mysql.review;
|
||||||
|
|
||||||
|
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.review.RequirementReviewDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface RequirementReviewMapper extends BaseMapperX<RequirementReviewDO> {
|
||||||
|
|
||||||
|
default RequirementReviewDO selectByObjectTypeAndRequirementId(String objectType, Long requirementId) {
|
||||||
|
return selectOne(new LambdaQueryWrapperX<RequirementReviewDO>()
|
||||||
|
.eq(RequirementReviewDO::getObjectType, objectType)
|
||||||
|
.eq(RequirementReviewDO::getRequirementId, requirementId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -79,6 +79,18 @@ public interface ObjectStatusTransitionMapper extends BaseMapperX<ObjectStatusTr
|
|||||||
.eq(ObjectStatusTransitionDO::getToStatusCode, statusCode)));
|
.eq(ObjectStatusTransitionDO::getToStatusCode, statusCode)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反查动作中文名:同 objectType + action_code 下 action_name 唯一一致(已核实),取任一行。
|
||||||
|
* 供错误提示等用户可见文案使用;查不到返回 null,由上层回退到原 actionCode。
|
||||||
|
*/
|
||||||
|
default String selectActionNameByObjectTypeAndAction(String objectType, String actionCode) {
|
||||||
|
List<ObjectStatusTransitionDO> list = selectList(new LambdaQueryWrapperX<ObjectStatusTransitionDO>()
|
||||||
|
.eq(ObjectStatusTransitionDO::getObjectType, objectType)
|
||||||
|
.eq(ObjectStatusTransitionDO::getActionCode, actionCode)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
return list.isEmpty() ? null : list.get(0).getActionName();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 物理删除
|
* 物理删除
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.njcn.rdms.module.project.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务到期范围快速筛选枚举(用于跨执行任务查询 chip)。
|
||||||
|
*
|
||||||
|
* <p>含义:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #OVERDUE} 计划完成日 < 今天,且任务状态非终态</li>
|
||||||
|
* <li>{@link #TODAY} 计划完成日 = 今天</li>
|
||||||
|
* <li>{@link #THIS_WEEK} 计划完成日 在本周(周一~周日,Asia/Shanghai)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>非终态的判定走 {@code ObjectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled('task')}
|
||||||
|
* 动态查询,不在此枚举里硬编码状态字面值。</p>
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum DueRangeEnum {
|
||||||
|
|
||||||
|
OVERDUE("overdue"),
|
||||||
|
TODAY("today"),
|
||||||
|
THIS_WEEK("thisWeek");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
public static DueRangeEnum of(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
for (DueRangeEnum e : values()) {
|
||||||
|
if (e.value.equals(value)) return e;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,4 +32,10 @@ public @interface CheckObjectPermission {
|
|||||||
*/
|
*/
|
||||||
boolean memberOnly() default false;
|
boolean memberOnly() default false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否走「可访问性门禁」:显式成员 OR 数据范围 scope 兜底(与 getXxxContext 入口口径一致)。
|
||||||
|
* 为 true 时切面调用 checkAccessible,忽略 permission / memberOnly(优先级 accessible > memberOnly > permission)。
|
||||||
|
*/
|
||||||
|
boolean accessible() default false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,13 @@ public class ObjectPermissionAspect {
|
|||||||
throw invalidParamException("暂不支持对象类型:{}", checkObjectPermission.objectType());
|
throw invalidParamException("暂不支持对象类型:{}", checkObjectPermission.objectType());
|
||||||
}
|
}
|
||||||
Long objectId = resolveObjectId(joinPoint, checkObjectPermission.objectId());
|
Long objectId = resolveObjectId(joinPoint, checkObjectPermission.objectId());
|
||||||
permissionService.checkPermission(objectId, checkObjectPermission.permission(),
|
// 分发优先级:accessible(可访问性门禁)> memberOnly / permission(权限码)
|
||||||
checkObjectPermission.memberOnly());
|
if (checkObjectPermission.accessible()) {
|
||||||
|
permissionService.checkAccessible(objectId);
|
||||||
|
} else {
|
||||||
|
permissionService.checkPermission(objectId, checkObjectPermission.permission(),
|
||||||
|
checkObjectPermission.memberOnly());
|
||||||
|
}
|
||||||
return joinPoint.proceed();
|
return joinPoint.proceed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,4 +19,24 @@ public interface ObjectPermissionService {
|
|||||||
*/
|
*/
|
||||||
void checkPermission(Long objectId, String permission, boolean memberOnly);
|
void checkPermission(Long objectId, String permission, boolean memberOnly);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前登录用户是否具备指定对象上的指定权限码(非抛模式)。
|
||||||
|
*
|
||||||
|
* 与 {@link #checkPermission(Long, String, boolean)} 区别:
|
||||||
|
* 本方法不抛异常,纯返回 boolean,用于"无权限就走降级路径"而非"无权限就 403"的场景。
|
||||||
|
*
|
||||||
|
* @param objectId 对象 ID(如 projectId)
|
||||||
|
* @param permission 权限码,如 {@code project:task:query}
|
||||||
|
* @return true=具备,false=不具备
|
||||||
|
*/
|
||||||
|
boolean hasPermission(Long objectId, String permission);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可访问性门禁:当前登录用户是否「能进入」该对象(显式成员 OR 数据范围 scope 兜底)。
|
||||||
|
* 不可访问(含对象不存在)一律抛 ..._OBJECT_PERMISSION_DENIED,不暴露对象是否存在(见 spec §3.3)。
|
||||||
|
*
|
||||||
|
* @param objectId 对象编号
|
||||||
|
*/
|
||||||
|
void checkAccessible(Long objectId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
|||||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||||
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
|
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||||
|
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
||||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -23,6 +28,7 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
/**
|
/**
|
||||||
* 产品对象权限服务。
|
* 产品对象权限服务。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class ProductObjectPermissionService implements ObjectPermissionService {
|
public class ProductObjectPermissionService implements ObjectPermissionService {
|
||||||
|
|
||||||
@@ -30,12 +36,25 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
|||||||
private UserObjectRoleMapper userObjectRoleMapper;
|
private UserObjectRoleMapper userObjectRoleMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private ObjectPermissionApi objectPermissionApi;
|
private ObjectPermissionApi objectPermissionApi;
|
||||||
|
@Resource
|
||||||
|
private ProductMapper productMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectDataScopeService objectDataScopeService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getObjectType() {
|
public String getObjectType() {
|
||||||
return ProductObjectConstants.OBJECT_TYPE;
|
return ProductObjectConstants.OBJECT_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(Long objectId, String permission) {
|
||||||
|
// 当前产品域无 hasPermission 非抛模式调用场景,预留空实现。
|
||||||
|
// 启用时参考 ProjectObjectPermissionService.hasPermission 同款实现。
|
||||||
|
log.warn("[ProductObjectPermissionService.hasPermission] 未实现,默认返回 false;objectId={}, permission={}",
|
||||||
|
objectId, permission);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkPermission(Long objectId, String permission, boolean memberOnly) {
|
public void checkPermission(Long objectId, String permission, boolean memberOnly) {
|
||||||
if (objectId == null) {
|
if (objectId == null) {
|
||||||
@@ -46,8 +65,9 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
|||||||
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||||
.selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
.selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
||||||
if (userRoles.isEmpty()) {
|
if (userRoles.isEmpty()) {
|
||||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED,
|
// 权限码/成员标记仅作技术诊断,落日志不外泄给用户(见 用户可见错误文案规范)
|
||||||
buildDeniedPermission(permission, memberOnly));
|
log.warn("[checkPermission] 用户无对象角色,objectId={}, permission={}, memberOnly={}", objectId, permission, memberOnly);
|
||||||
|
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||||
}
|
}
|
||||||
if (memberOnly) {
|
if (memberOnly) {
|
||||||
return;
|
return;
|
||||||
@@ -59,7 +79,34 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
|||||||
.distinct()
|
.distinct()
|
||||||
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED, normalizedPermission);
|
log.warn("[checkPermission] 缺少对象权限码,objectId={}, permission={}", objectId, normalizedPermission);
|
||||||
|
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkAccessible(Long objectId) {
|
||||||
|
if (objectId == null) {
|
||||||
|
throw invalidParamException("对象编号不能为空");
|
||||||
|
}
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
// 显式成员:拥有任一 ACTIVE 对象角色即可访问
|
||||||
|
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||||
|
.selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
||||||
|
if (!userRoles.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 无显式角色:查对象拿 directionCode,按数据范围 scope 兜底(隐式 observer / 超管 ALL)
|
||||||
|
ProductDO product = productMapper.selectById(objectId);
|
||||||
|
if (product == null) {
|
||||||
|
// spec §3.3 定稿:对象不存在一律 DENIED,不暴露存在性(技术诊断落 log.warn)
|
||||||
|
log.warn("[checkAccessible] 对象不存在或无访问权,objectId={}", objectId);
|
||||||
|
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
|
||||||
|
if (!scope.contains(objectId, product.getDirectionCode())) {
|
||||||
|
log.warn("[checkAccessible] 无对象访问权,objectId={}", objectId);
|
||||||
|
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +131,4 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
|||||||
return permission.trim();
|
return permission.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildDeniedPermission(String permission, boolean memberOnly) {
|
|
||||||
return memberOnly ? "member" : normalizePermission(permission);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package com.njcn.rdms.module.project.framework.security.service;
|
package com.njcn.rdms.module.project.framework.security.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
|
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||||
|
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
||||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -23,6 +29,7 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
/**
|
/**
|
||||||
* 项目对象权限服务。
|
* 项目对象权限服务。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class ProjectObjectPermissionService implements ObjectPermissionService {
|
public class ProjectObjectPermissionService implements ObjectPermissionService {
|
||||||
|
|
||||||
@@ -30,6 +37,10 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
|||||||
private UserObjectRoleMapper userObjectRoleMapper;
|
private UserObjectRoleMapper userObjectRoleMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private ObjectPermissionApi objectPermissionApi;
|
private ObjectPermissionApi objectPermissionApi;
|
||||||
|
@Resource
|
||||||
|
private ProjectMapper projectMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectDataScopeService objectDataScopeService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getObjectType() {
|
public String getObjectType() {
|
||||||
@@ -46,20 +57,75 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
|||||||
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||||
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
||||||
if (userRoles.isEmpty()) {
|
if (userRoles.isEmpty()) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED,
|
// 权限码/成员标记仅作技术诊断,落日志不外泄给用户(见 用户可见错误文案规范)
|
||||||
buildDeniedPermission(permission, memberOnly));
|
log.warn("[checkPermission] 用户无对象角色,objectId={}, permission={}, memberOnly={}", objectId, permission, memberOnly);
|
||||||
|
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||||
}
|
}
|
||||||
if (memberOnly) {
|
if (memberOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String normalizedPermission = normalizePermission(permission);
|
String normalizedPermission = normalizePermission(permission);
|
||||||
// 任一角色含该权限码即放行(等价于多角色 union;短路求值,权限码命中早 return)
|
// 任一角色含该权限码即放行(等价于多角色 union;短路求值)
|
||||||
boolean allowed = userRoles.stream()
|
boolean allowed = userRoles.stream()
|
||||||
.map(UserObjectRoleDO::getRoleId)
|
.map(UserObjectRoleDO::getRoleId)
|
||||||
.distinct()
|
.distinct()
|
||||||
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED, normalizedPermission);
|
log.warn("[checkPermission] 缺少对象权限码,objectId={}, permission={}", objectId, normalizedPermission);
|
||||||
|
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkAccessible(Long objectId) {
|
||||||
|
if (objectId == null) {
|
||||||
|
throw invalidParamException("对象编号不能为空");
|
||||||
|
}
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
// 显式成员:拥有任一 ACTIVE 对象角色即可访问
|
||||||
|
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||||
|
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, loginUserId);
|
||||||
|
if (!userRoles.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 无显式角色:查对象拿 directionCode,按数据范围 scope 兜底(隐式 observer / 超管 ALL)
|
||||||
|
ProjectDO project = projectMapper.selectById(objectId);
|
||||||
|
if (project == null) {
|
||||||
|
// spec §3.3 定稿:对象不存在一律 DENIED,不暴露存在性(技术诊断落 log.warn)
|
||||||
|
log.warn("[checkAccessible] 对象不存在或无访问权,objectId={}", objectId);
|
||||||
|
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProjectObjectConstants.OBJECT_TYPE);
|
||||||
|
if (!scope.contains(objectId, project.getDirectionCode())) {
|
||||||
|
log.warn("[checkAccessible] 无对象访问权,objectId={}", objectId);
|
||||||
|
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(Long objectId, String permission) {
|
||||||
|
if (objectId == null || StrUtil.isBlank(permission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 先确认是对象成员,非成员直接返回 false
|
||||||
|
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||||
|
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, userId);
|
||||||
|
if (userRoles.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalizedPermission = permission.trim();
|
||||||
|
return userRoles.stream()
|
||||||
|
.map(UserObjectRoleDO::getRoleId)
|
||||||
|
.distinct()
|
||||||
|
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[hasPermission] objectId={}, permission={} 查询权限失败", objectId, permission, e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +149,4 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
|||||||
return permission.trim();
|
return permission.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildDeniedPermission(String permission, boolean memberOnly) {
|
|
||||||
return memberOnly ? "member" : normalizePermission(permission);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.njcn.rdms.module.project.service.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusDictRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface OvertimeApplicationService {
|
||||||
|
|
||||||
|
Long createApplication(OvertimeApplicationSaveReqVO reqVO);
|
||||||
|
|
||||||
|
void resubmitApplication(Long id, OvertimeApplicationSaveReqVO reqVO);
|
||||||
|
|
||||||
|
void approve(Long id, OvertimeApplicationStatusActionReqVO reqVO);
|
||||||
|
|
||||||
|
void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO);
|
||||||
|
|
||||||
|
void cancel(Long id, OvertimeApplicationStatusActionReqVO reqVO);
|
||||||
|
|
||||||
|
void deleteApplication(Long id);
|
||||||
|
|
||||||
|
OvertimeApplicationRespVO getApplication(Long id);
|
||||||
|
|
||||||
|
List<OvertimeApplicationStatusDictRespVO> getStatusDict();
|
||||||
|
|
||||||
|
PageResult<OvertimeApplicationRespVO> getMyPage(OvertimeApplicationPageReqVO reqVO);
|
||||||
|
|
||||||
|
PageResult<OvertimeApplicationRespVO> getApprovalPage(OvertimeApplicationPageReqVO reqVO);
|
||||||
|
|
||||||
|
List<OvertimeApplicationStatusLogRespVO> getStatusLogs(Long id);
|
||||||
|
|
||||||
|
List<OvertimeApplicationExportVO> getExportList(OvertimeApplicationPageReqVO reqVO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,496 @@
|
|||||||
|
package com.njcn.rdms.module.project.service.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||||
|
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||||
|
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusDictRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationStatusLogDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationStatusLogMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||||
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
|
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||||
|
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||||
|
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
|
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OvertimeApplicationServiceImpl implements OvertimeApplicationService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OvertimeApplicationMapper overtimeApplicationMapper;
|
||||||
|
@Resource
|
||||||
|
private OvertimeApplicationStatusLogMapper overtimeApplicationStatusLogMapper;
|
||||||
|
@Resource
|
||||||
|
private BizAuditLogMapper bizAuditLogMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
||||||
|
@Resource
|
||||||
|
private StatusActionTextResolver statusActionTextResolver;
|
||||||
|
@Resource
|
||||||
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long createApplication(OvertimeApplicationSaveReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
AdminUserRespDTO approver = validateApprover(reqVO.getApproverId());
|
||||||
|
String initialStatus = getInitialStatusCode();
|
||||||
|
|
||||||
|
OvertimeApplicationDO application = new OvertimeApplicationDO();
|
||||||
|
application.setApplicantId(loginUserId);
|
||||||
|
application.setApplicantName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||||
|
applySaveFields(application, reqVO, approver);
|
||||||
|
application.setStatusCode(initialStatus);
|
||||||
|
application.setApprovalComment(null);
|
||||||
|
application.setSubmitTime(LocalDateTime.now());
|
||||||
|
application.setApprovalTime(null);
|
||||||
|
overtimeApplicationMapper.insert(application);
|
||||||
|
|
||||||
|
// 创建即进入初始状态 pending,这里保留 submit 作为业务留痕动作,不依赖状态机流转表存在 submit 配置。
|
||||||
|
writeStatusLog(application, OvertimeApplicationConstants.ACTION_SUBMIT, null, initialStatus, null);
|
||||||
|
writeAuditLog(application, OvertimeApplicationConstants.ACTION_SUBMIT, null, initialStatus, null, null, null);
|
||||||
|
return application.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void resubmitApplication(Long id, OvertimeApplicationSaveReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
OvertimeApplicationDO current = validateApplicationExists(id);
|
||||||
|
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
|
||||||
|
}
|
||||||
|
String fromStatus = current.getStatusCode();
|
||||||
|
ObjectStatusTransitionDO transition = validateTransition(fromStatus, OvertimeApplicationConstants.ACTION_RESUBMIT,
|
||||||
|
null);
|
||||||
|
AdminUserRespDTO approver = validateApprover(reqVO.getApproverId());
|
||||||
|
|
||||||
|
OvertimeApplicationDO before = cloneApplication(current);
|
||||||
|
OvertimeApplicationDO update = new OvertimeApplicationDO();
|
||||||
|
applySaveFields(update, reqVO, approver);
|
||||||
|
update.setStatusCode(transition.getToStatusCode());
|
||||||
|
update.setApprovalComment(null);
|
||||||
|
update.setSubmitTime(LocalDateTime.now());
|
||||||
|
update.setApprovalTime(null);
|
||||||
|
|
||||||
|
int updateCount = overtimeApplicationMapper.updateByIdAndStatusesAndApplicantId(update, id,
|
||||||
|
List.of(OvertimeApplicationConstants.STATUS_REJECTED, OvertimeApplicationConstants.STATUS_CANCELLED),
|
||||||
|
loginUserId);
|
||||||
|
if (updateCount != 1) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
OvertimeApplicationDO after = mergeUpdated(current, update);
|
||||||
|
writeStatusLog(after, OvertimeApplicationConstants.ACTION_RESUBMIT, fromStatus, transition.getToStatusCode(),
|
||||||
|
null);
|
||||||
|
writeAuditLog(after, OvertimeApplicationConstants.ACTION_RESUBMIT, fromStatus, transition.getToStatusCode(),
|
||||||
|
buildFieldChanges(before, after), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void approve(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
processApprovalAction(id, OvertimeApplicationConstants.ACTION_APPROVE, reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
processApprovalAction(id, OvertimeApplicationConstants.ACTION_REJECT, reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void cancel(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
OvertimeApplicationDO current = validateApplicationExists(id);
|
||||||
|
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
|
||||||
|
}
|
||||||
|
String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason());
|
||||||
|
String fromStatus = current.getStatusCode();
|
||||||
|
ObjectStatusTransitionDO transition = validateTransition(fromStatus, OvertimeApplicationConstants.ACTION_CANCEL,
|
||||||
|
reason);
|
||||||
|
|
||||||
|
OvertimeApplicationDO update = new OvertimeApplicationDO();
|
||||||
|
update.setStatusCode(transition.getToStatusCode());
|
||||||
|
update.setApprovalComment(reason);
|
||||||
|
update.setApprovalTime(LocalDateTime.now());
|
||||||
|
int updateCount = overtimeApplicationMapper.updateByIdAndStatusAndApplicantId(update, id, fromStatus,
|
||||||
|
loginUserId);
|
||||||
|
if (updateCount != 1) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
OvertimeApplicationDO after = mergeUpdated(current, update);
|
||||||
|
writeStatusLog(after, OvertimeApplicationConstants.ACTION_CANCEL, fromStatus, transition.getToStatusCode(),
|
||||||
|
reason);
|
||||||
|
writeAuditLog(after, OvertimeApplicationConstants.ACTION_CANCEL, fromStatus, transition.getToStatusCode(),
|
||||||
|
null, reason, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteApplication(Long id) {
|
||||||
|
OvertimeApplicationDO current = validateApplicationExists(id);
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
|
||||||
|
}
|
||||||
|
if (!OvertimeApplicationConstants.STATUS_CANCELLED.equals(current.getStatusCode())) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED);
|
||||||
|
}
|
||||||
|
overtimeApplicationMapper.deleteById(id);
|
||||||
|
writeAuditLog(current, OvertimeApplicationConstants.ACTION_DELETE, current.getStatusCode(), null, null, null,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OvertimeApplicationRespVO getApplication(Long id) {
|
||||||
|
OvertimeApplicationDO application = validateReadableApplication(id);
|
||||||
|
return toRespVO(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OvertimeApplicationStatusDictRespVO> getStatusDict() {
|
||||||
|
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
|
||||||
|
.selectListByObjectTypeEnabled(OvertimeApplicationConstants.STATUS_OBJECT_TYPE);
|
||||||
|
return statusModels.stream()
|
||||||
|
.map(this::buildStatusDictRespVO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<OvertimeApplicationRespVO> getMyPage(OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
PageResult<OvertimeApplicationDO> page = overtimeApplicationMapper.selectMyPage(loginUserId, reqVO);
|
||||||
|
return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<OvertimeApplicationRespVO> getApprovalPage(OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
PageResult<OvertimeApplicationDO> page = overtimeApplicationMapper.selectApprovalPage(loginUserId, reqVO);
|
||||||
|
return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OvertimeApplicationStatusLogRespVO> getStatusLogs(Long id) {
|
||||||
|
validateReadableApplication(id);
|
||||||
|
return BeanUtils.toBean(overtimeApplicationStatusLogMapper.selectListByApplicationId(id),
|
||||||
|
OvertimeApplicationStatusLogRespVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OvertimeApplicationExportVO> getExportList(OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||||
|
PageResult<OvertimeApplicationRespVO> page = getMyPage(reqVO);
|
||||||
|
return BeanUtils.toBean(page.getList(), OvertimeApplicationExportVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processApprovalAction(Long id, String actionCode, OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
OvertimeApplicationDO current = validateApplicationExists(id);
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (!Objects.equals(current.getApproverId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_ONLY);
|
||||||
|
}
|
||||||
|
String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason());
|
||||||
|
String fromStatus = current.getStatusCode();
|
||||||
|
ObjectStatusTransitionDO transition = validateTransition(fromStatus, actionCode, reason);
|
||||||
|
|
||||||
|
OvertimeApplicationDO update = new OvertimeApplicationDO();
|
||||||
|
update.setStatusCode(transition.getToStatusCode());
|
||||||
|
update.setApprovalComment(reason);
|
||||||
|
update.setApprovalTime(LocalDateTime.now());
|
||||||
|
int updateCount = overtimeApplicationMapper.updateByIdAndStatus(update, id, fromStatus);
|
||||||
|
if (updateCount != 1) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
OvertimeApplicationDO after = mergeUpdated(current, update);
|
||||||
|
writeStatusLog(after, actionCode, fromStatus, transition.getToStatusCode(), reason);
|
||||||
|
writeAuditLog(after, actionCode, fromStatus, transition.getToStatusCode(), null, reason, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationDO validateApplicationExists(Long id) {
|
||||||
|
OvertimeApplicationDO application = overtimeApplicationMapper.selectById(id);
|
||||||
|
if (application == null) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationDO validateReadableApplication(Long id) {
|
||||||
|
OvertimeApplicationDO application = validateApplicationExists(id);
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (!Objects.equals(application.getApplicantId(), loginUserId)
|
||||||
|
&& !Objects.equals(application.getApproverId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_READ_FORBIDDEN);
|
||||||
|
}
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) {
|
||||||
|
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||||
|
.selectByObjectTypeAndFromStatusAndAction(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, fromStatus,
|
||||||
|
actionCode);
|
||||||
|
if (transition == null) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, fromStatus),
|
||||||
|
statusActionTextResolver.actionName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, actionCode));
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED,
|
||||||
|
statusActionTextResolver.actionName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, actionCode));
|
||||||
|
}
|
||||||
|
ObjectStatusModelDO toModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||||
|
OvertimeApplicationConstants.STATUS_OBJECT_TYPE, transition.getToStatusCode());
|
||||||
|
if (toModel == null) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||||
|
}
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getInitialStatusCode() {
|
||||||
|
ObjectStatusModelDO statusModel = objectStatusModelMapper
|
||||||
|
.selectInitialByObjectTypeEnabled(OvertimeApplicationConstants.STATUS_OBJECT_TYPE);
|
||||||
|
if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||||
|
}
|
||||||
|
return statusModel.getStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminUserRespDTO validateApprover(Long approverId) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (Objects.equals(approverId, loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN);
|
||||||
|
}
|
||||||
|
return loadUser(approverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminUserRespDTO loadUser(Long userId) {
|
||||||
|
if (userId == null) {
|
||||||
|
throw invalidParamException("用户编号不能为空");
|
||||||
|
}
|
||||||
|
adminUserApi.validateUserList(Collections.singleton(userId)).getCheckedData();
|
||||||
|
CommonResult<AdminUserRespDTO> result = adminUserApi.getUser(userId);
|
||||||
|
AdminUserRespDTO user = result.getCheckedData();
|
||||||
|
if (user == null) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_INVALID);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySaveFields(OvertimeApplicationDO target, OvertimeApplicationSaveReqVO reqVO,
|
||||||
|
AdminUserRespDTO approver) {
|
||||||
|
target.setOvertimeDate(reqVO.getOvertimeDate());
|
||||||
|
target.setOvertimeDuration(normalizeRequiredText(reqVO.getOvertimeDuration(), "加班时长不能为空"));
|
||||||
|
target.setOvertimeReason(normalizeRequiredText(reqVO.getOvertimeReason(), "加班原因不能为空"));
|
||||||
|
target.setOvertimeContent(normalizeRequiredText(reqVO.getOvertimeContent(), "加班内容不能为空"));
|
||||||
|
target.setApproverId(approver.getId());
|
||||||
|
target.setApproverName(defaultText(approver.getNickname()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationRespVO toRespVO(OvertimeApplicationDO application) {
|
||||||
|
return BeanUtils.toBean(application, OvertimeApplicationRespVO.class, this::applyStatusView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyStatusView(OvertimeApplicationRespVO respVO) {
|
||||||
|
ObjectStatusModelDO statusModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||||
|
OvertimeApplicationConstants.STATUS_OBJECT_TYPE, respVO.getStatusCode());
|
||||||
|
if (statusModel == null) {
|
||||||
|
respVO.setStatusName(respVO.getStatusCode());
|
||||||
|
respVO.setAllowEdit(false);
|
||||||
|
respVO.setTerminal(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
respVO.setStatusName(statusModel.getStatusName());
|
||||||
|
respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit()));
|
||||||
|
respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationStatusDictRespVO buildStatusDictRespVO(ObjectStatusModelDO statusModel) {
|
||||||
|
OvertimeApplicationStatusDictRespVO respVO = new OvertimeApplicationStatusDictRespVO();
|
||||||
|
respVO.setStatusCode(statusModel.getStatusCode());
|
||||||
|
respVO.setStatusName(statusModel.getStatusName());
|
||||||
|
respVO.setSort(statusModel.getSort());
|
||||||
|
respVO.setInitialFlag(statusModel.getInitialFlag());
|
||||||
|
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
||||||
|
respVO.setAllowEdit(statusModel.getAllowEdit());
|
||||||
|
return respVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeStatusLog(OvertimeApplicationDO application, String actionType, String fromStatus,
|
||||||
|
String toStatus, String reason) {
|
||||||
|
OvertimeApplicationStatusLogDO log = new OvertimeApplicationStatusLogDO();
|
||||||
|
log.setApplicationId(application.getId());
|
||||||
|
log.setActionType(actionType);
|
||||||
|
log.setFromStatus(fromStatus);
|
||||||
|
log.setToStatus(toStatus);
|
||||||
|
log.setReason(reason);
|
||||||
|
log.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||||
|
log.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||||
|
log.setApplicantNameSnapshot(application.getApplicantName());
|
||||||
|
log.setOvertimeDateSnapshot(application.getOvertimeDate());
|
||||||
|
log.setOvertimeDurationSnapshot(application.getOvertimeDuration());
|
||||||
|
log.setRemark(buildSnapshotRemark(application));
|
||||||
|
overtimeApplicationStatusLogMapper.insert(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeAuditLog(OvertimeApplicationDO application, String actionType, String fromStatus,
|
||||||
|
String toStatus, String fieldChanges, String reason, String remark) {
|
||||||
|
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||||
|
auditLog.setBizType(OvertimeApplicationConstants.BIZ_TYPE);
|
||||||
|
auditLog.setBizId(application.getId());
|
||||||
|
auditLog.setActionType(actionType);
|
||||||
|
auditLog.setFromStatus(fromStatus);
|
||||||
|
auditLog.setToStatus(toStatus);
|
||||||
|
auditLog.setFieldChanges(fieldChanges);
|
||||||
|
auditLog.setReason(reason);
|
||||||
|
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||||
|
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||||
|
auditLog.setRemark(StringUtils.hasText(remark) ? remark : buildSnapshotRemark(application));
|
||||||
|
bizAuditLogMapper.insert(auditLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildSnapshotRemark(OvertimeApplicationDO application) {
|
||||||
|
return "申请人:" + defaultText(application.getApplicantName())
|
||||||
|
+ ",加班日期:" + application.getOvertimeDate()
|
||||||
|
+ ",加班时长:" + defaultText(application.getOvertimeDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildFieldChanges(OvertimeApplicationDO before, OvertimeApplicationDO after) {
|
||||||
|
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||||
|
appendFieldChange(fieldChanges, "overtimeDate", valueOf(before, OvertimeApplicationDO::getOvertimeDate),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getOvertimeDate));
|
||||||
|
appendFieldChange(fieldChanges, "overtimeDuration",
|
||||||
|
valueOf(before, OvertimeApplicationDO::getOvertimeDuration),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getOvertimeDuration));
|
||||||
|
appendFieldChange(fieldChanges, "overtimeReason", valueOf(before, OvertimeApplicationDO::getOvertimeReason),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getOvertimeReason));
|
||||||
|
appendFieldChange(fieldChanges, "overtimeContent", valueOf(before, OvertimeApplicationDO::getOvertimeContent),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getOvertimeContent));
|
||||||
|
appendFieldChange(fieldChanges, "approverId", valueOf(before, OvertimeApplicationDO::getApproverId),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getApproverId));
|
||||||
|
appendFieldChange(fieldChanges, "approverName", valueOf(before, OvertimeApplicationDO::getApproverName),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getApproverName));
|
||||||
|
appendFieldChange(fieldChanges, "statusCode", valueOf(before, OvertimeApplicationDO::getStatusCode),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getStatusCode));
|
||||||
|
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
|
||||||
|
if (Objects.equals(before, after)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> value = new LinkedHashMap<>();
|
||||||
|
value.put("before", before);
|
||||||
|
value.put("after", after);
|
||||||
|
fieldChanges.put(fieldName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T valueOf(OvertimeApplicationDO application, Function<OvertimeApplicationDO, T> getter) {
|
||||||
|
return application == null ? null : getter.apply(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationDO cloneApplication(OvertimeApplicationDO source) {
|
||||||
|
OvertimeApplicationDO target = new OvertimeApplicationDO();
|
||||||
|
target.setId(source.getId());
|
||||||
|
target.setApplicantId(source.getApplicantId());
|
||||||
|
target.setApplicantName(source.getApplicantName());
|
||||||
|
target.setOvertimeDate(source.getOvertimeDate());
|
||||||
|
target.setOvertimeDuration(source.getOvertimeDuration());
|
||||||
|
target.setOvertimeReason(source.getOvertimeReason());
|
||||||
|
target.setOvertimeContent(source.getOvertimeContent());
|
||||||
|
target.setApproverId(source.getApproverId());
|
||||||
|
target.setApproverName(source.getApproverName());
|
||||||
|
target.setStatusCode(source.getStatusCode());
|
||||||
|
target.setApprovalComment(source.getApprovalComment());
|
||||||
|
target.setSubmitTime(source.getSubmitTime());
|
||||||
|
target.setApprovalTime(source.getApprovalTime());
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationDO mergeUpdated(OvertimeApplicationDO current, OvertimeApplicationDO update) {
|
||||||
|
OvertimeApplicationDO after = cloneApplication(current);
|
||||||
|
if (update.getOvertimeDate() != null) {
|
||||||
|
after.setOvertimeDate(update.getOvertimeDate());
|
||||||
|
}
|
||||||
|
if (update.getOvertimeDuration() != null) {
|
||||||
|
after.setOvertimeDuration(update.getOvertimeDuration());
|
||||||
|
}
|
||||||
|
if (update.getOvertimeReason() != null) {
|
||||||
|
after.setOvertimeReason(update.getOvertimeReason());
|
||||||
|
}
|
||||||
|
if (update.getOvertimeContent() != null) {
|
||||||
|
after.setOvertimeContent(update.getOvertimeContent());
|
||||||
|
}
|
||||||
|
if (update.getApproverId() != null) {
|
||||||
|
after.setApproverId(update.getApproverId());
|
||||||
|
}
|
||||||
|
if (update.getApproverName() != null) {
|
||||||
|
after.setApproverName(update.getApproverName());
|
||||||
|
}
|
||||||
|
if (update.getStatusCode() != null) {
|
||||||
|
after.setStatusCode(update.getStatusCode());
|
||||||
|
}
|
||||||
|
after.setApprovalComment(update.getApprovalComment());
|
||||||
|
if (update.getSubmitTime() != null) {
|
||||||
|
after.setSubmitTime(update.getSubmitTime());
|
||||||
|
}
|
||||||
|
after.setApprovalTime(update.getApprovalTime());
|
||||||
|
return after;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRequiredText(String value, String message) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
throw invalidParamException(message);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeNullableText(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultText(String value) {
|
||||||
|
return StringUtils.hasText(value) ? value : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMappe
|
|||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
|
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
|
||||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
|
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
|
||||||
|
import com.njcn.rdms.module.project.service.project.ProjectService;
|
||||||
|
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -83,6 +85,10 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||||
@Resource
|
@Resource
|
||||||
private AdminUserApi adminUserApi;
|
private AdminUserApi adminUserApi;
|
||||||
|
@Resource
|
||||||
|
private StatusActionTextResolver statusActionTextResolver;
|
||||||
|
@Resource
|
||||||
|
private ProjectService projectService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -170,11 +176,13 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||||
.selectByObjectTypeAndFromStatusAndAction(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode);
|
.selectByObjectTypeAndFromStatusAndAction(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode);
|
||||||
if (transition == null) {
|
if (transition == null) {
|
||||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus),
|
||||||
|
statusActionTextResolver.actionName(PersonalItemConstants.STATUS_OBJECT_TYPE, actionCode));
|
||||||
}
|
}
|
||||||
String reason = normalizeNullableText(reqVO.getReason());
|
String reason = normalizeNullableText(reqVO.getReason());
|
||||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED, actionCode);
|
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||||
}
|
}
|
||||||
String toStatus = transition.getToStatusCode();
|
String toStatus = transition.getToStatusCode();
|
||||||
int updateCount = personalItemMapper.updateStatusByIdAndStatus(item.getId(), fromStatus, toStatus, reason);
|
int updateCount = personalItemMapper.updateStatusByIdAndStatus(item.getId(), fromStatus, toStatus, reason);
|
||||||
@@ -354,6 +362,8 @@ public class PersonalItemServiceImpl implements PersonalItemService {
|
|||||||
writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_DELETE,
|
writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_DELETE,
|
||||||
item.getStatusCode(), null, null, "关联执行后转为项目任务");
|
item.getStatusCode(), null, null, "关联执行后转为项目任务");
|
||||||
}
|
}
|
||||||
|
// 个人事项转为项目根任务后,项目根任务均值已变,需下推一次项目进度重算(与任务侧增删改入口口径一致)
|
||||||
|
projectService.recalcProgress(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProjectTaskDO buildProjectTaskFromItem(PersonalItemDO item, Long projectId, Long executionId) {
|
private ProjectTaskDO buildProjectTaskFromItem(PersonalItemDO item, Long projectId, Long executionId) {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
|
|||||||
private AdminUserApi adminUserApi;
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", accessible = true)
|
||||||
public List<ProductMemberRespVO> getProductMemberList(Long productId) {
|
public List<ProductMemberRespVO> getProductMemberList(Long productId) {
|
||||||
ProductDO product = validateProductExists(productId);
|
ProductDO product = validateProductExists(productId);
|
||||||
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProductObjectConstants.OBJECT_TYPE, productId);
|
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProductObjectConstants.OBJECT_TYPE, productId);
|
||||||
|
|||||||
@@ -10,171 +10,48 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public interface ProductRequirementService {
|
public interface ProductRequirementService {
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建产品需求
|
|
||||||
*
|
|
||||||
* @param createReqVO 创建请求
|
|
||||||
* @return 需求编号
|
|
||||||
*/
|
|
||||||
Long createRequirement(ProductRequirementSaveReqVO createReqVO);
|
Long createRequirement(ProductRequirementSaveReqVO createReqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新产品需求(不含状态变更)
|
|
||||||
*
|
|
||||||
* @param updateReqVO 更新请求
|
|
||||||
*/
|
|
||||||
void updateRequirement(ProductRequirementUpdateReqVO updateReqVO);
|
void updateRequirement(ProductRequirementUpdateReqVO updateReqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求详情
|
|
||||||
*
|
|
||||||
* @param id 需求编号
|
|
||||||
* @return 需求详情
|
|
||||||
*/
|
|
||||||
ProductRequirementRespVO getRequirement(Long id, Long productId);
|
ProductRequirementRespVO getRequirement(Long id, Long productId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求分页列表
|
|
||||||
*
|
|
||||||
* @param pageReqVO 分页请求
|
|
||||||
* @return 分页结果
|
|
||||||
*/
|
|
||||||
PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO);
|
PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求树形列表(分页)
|
|
||||||
*
|
|
||||||
* @param pageReqVO 分页请求
|
|
||||||
* @return 分页结果(只按父需求分页,子需求不计入分页)
|
|
||||||
*/
|
|
||||||
PageResult<ProductRequirementRespVO> getRequirementTree(ProductRequirementPageReqVO pageReqVO);
|
PageResult<ProductRequirementRespVO> getRequirementTree(ProductRequirementPageReqVO pageReqVO);
|
||||||
|
|
||||||
/**
|
ProductRequirementDashboardRespVO getRequirementDashboard(Long productId);
|
||||||
* 变更需求状态
|
|
||||||
*
|
|
||||||
* @param reqVO 状态动作请求
|
|
||||||
*/
|
|
||||||
void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO);
|
void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO);
|
||||||
|
|
||||||
/**
|
void changeRequirementStatusForReview(ProductRequirementStatusActionReqVO reqVO);
|
||||||
* 删除需求
|
|
||||||
*
|
|
||||||
* @param id 需求编号
|
|
||||||
* @param productId 产品编号
|
|
||||||
*/
|
|
||||||
void deleteRequirement(Long id, Long productId);
|
void deleteRequirement(Long id, Long productId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 拆分需求(创建子需求)
|
|
||||||
*
|
|
||||||
* @param reqVO 拆分请求
|
|
||||||
* @return 子需求编号
|
|
||||||
*/
|
|
||||||
Long splitRequirement(ProductRequirementSplitReqVO reqVO);
|
Long splitRequirement(ProductRequirementSplitReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭需求(大需求关闭时级联关闭子需求)
|
|
||||||
*
|
|
||||||
* @param reqVO 关闭请求
|
|
||||||
*/
|
|
||||||
void closeRequirement(ProductRequirementCloseReqVO reqVO);
|
void closeRequirement(ProductRequirementCloseReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求当前可执行的状态动作列表
|
|
||||||
*
|
|
||||||
* @param requirementId 需求编号
|
|
||||||
* @param productId 产品编号
|
|
||||||
* @return 可执行动作列表
|
|
||||||
*/
|
|
||||||
List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId);
|
List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量获取需求当前可执行的状态动作列表
|
|
||||||
*
|
|
||||||
* @param reqVO 批量查询请求
|
|
||||||
* @return 按需求编号标识的可执行动作列表
|
|
||||||
*/
|
|
||||||
List<ProductRequirementAllowedTransitionBatchRespVO> getAllowedTransitionsBatch(ProductRequirementBatchReqVO reqVO);
|
List<ProductRequirementAllowedTransitionBatchRespVO> getAllowedTransitionsBatch(ProductRequirementBatchReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断需求是否已分流并生成项目需求
|
|
||||||
*
|
|
||||||
* @param requirementId 需求编号
|
|
||||||
* @param productId 产品编号
|
|
||||||
* @return 是否已分流
|
|
||||||
*/
|
|
||||||
boolean hasDispatchedProjectRequirement(Long requirementId, Long productId);
|
boolean hasDispatchedProjectRequirement(Long requirementId, Long productId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量判断需求是否已分流并生成项目需求
|
|
||||||
*
|
|
||||||
* @param reqVO 批量查询请求
|
|
||||||
* @return 按需求编号标识的分流状态
|
|
||||||
*/
|
|
||||||
List<ProductRequirementHasDispatchedBatchRespVO> hasDispatchedProjectRequirementBatch(ProductRequirementBatchReqVO reqVO);
|
List<ProductRequirementHasDispatchedBatchRespVO> hasDispatchedProjectRequirementBatch(ProductRequirementBatchReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求生命周期信息(当前状态 + 可执行动作)
|
|
||||||
*
|
|
||||||
* @param requirementId 需求编号
|
|
||||||
* @param productId 产品编号
|
|
||||||
* @return 生命周期信息
|
|
||||||
*/
|
|
||||||
ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId);
|
|
||||||
|
|
||||||
// ========== 模块管理 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建需求模块
|
|
||||||
*
|
|
||||||
* @param reqVO 模块保存请求
|
|
||||||
* @return 模块编号
|
|
||||||
*/
|
|
||||||
Long createRequirementModule(ProductRequirementModuleReqVO reqVO);
|
Long createRequirementModule(ProductRequirementModuleReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新需求模块
|
|
||||||
*
|
|
||||||
* @param reqVO 模块保存请求
|
|
||||||
*/
|
|
||||||
void updateRequirementModule(ProductRequirementModuleReqVO reqVO);
|
void updateRequirementModule(ProductRequirementModuleReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除需求模块(级联删除模块下需求)
|
|
||||||
*
|
|
||||||
* @param moduleId 模块编号
|
|
||||||
* @param productId 产品编号
|
|
||||||
*/
|
|
||||||
void deleteRequirementModule(Long moduleId, Long productId);
|
void deleteRequirementModule(Long moduleId, Long productId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求模块树
|
|
||||||
*
|
|
||||||
* @param productId 产品编号
|
|
||||||
* @return 模块树
|
|
||||||
*/
|
|
||||||
List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId);
|
List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求所有状态字典列表
|
|
||||||
*
|
|
||||||
* @return 状态字典列表
|
|
||||||
*/
|
|
||||||
List<ProductRequirementStatusDictRespVO> getRequirementStatusDict();
|
List<ProductRequirementStatusDictRespVO> getRequirementStatusDict();
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求终止态状态字典列表
|
|
||||||
*
|
|
||||||
* @return 终止态状态字典列表
|
|
||||||
*/
|
|
||||||
List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
|
List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取产品需求分流后对应的项目需求跳转链接
|
|
||||||
*
|
|
||||||
* @param productRequirementId 产品需求编号
|
|
||||||
* @return 项目需求ID和关联项目ID
|
|
||||||
*/
|
|
||||||
ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId);
|
ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.njcn.rdms.module.project.service.product;
|
package com.njcn.rdms.module.project.service.product;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
||||||
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
|
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
|
||||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||||
@@ -35,6 +37,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -53,21 +56,21 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
private static final String PRODUCT_OBJECT_TYPE = "product";
|
private static final String PRODUCT_OBJECT_TYPE = "product";
|
||||||
|
|
||||||
// 需求状态常量
|
// 需求状态常量
|
||||||
private static final String STATUS_PENDING_CONFIRM = "pending_confirm";
|
private static final String STATUS_PENDING_CLAIM = "pending_claim";
|
||||||
private static final String STATUS_PENDING_REVIEW = "pending_review";
|
private static final String STATUS_PENDING_REVIEW = "pending_review";
|
||||||
private static final String STATUS_PENDING_DISPATCH = "pending_dispatch";
|
private static final String STATUS_PENDING_DISPATCH = "pending_dispatch";
|
||||||
|
private static final String STATUS_REVIEWED = "reviewed";
|
||||||
private static final String STATUS_IMPLEMENTING = "implementing";
|
private static final String STATUS_IMPLEMENTING = "implementing";
|
||||||
private static final String STATUS_ACCEPTED = "accepted";
|
private static final String STATUS_ACCEPTED = "accepted";
|
||||||
private static final String STATUS_CLOSED = "closed";
|
private static final String STATUS_CLOSED = "closed";
|
||||||
private static final String STATUS_REJECTED = "rejected";
|
private static final String STATUS_REJECTED = "rejected";
|
||||||
private static final String STATUS_CANCELLED = "cancelled";
|
private static final String STATUS_CANCELLED = "cancelled";
|
||||||
|
private static final String STATUS_REVIEW_REJECTED = "review_rejected";
|
||||||
|
private static final String SOURCE_TYPE_MANUAL = "manual";
|
||||||
|
private static final String SOURCE_TYPE_WORK_ORDER = "work_order";
|
||||||
|
|
||||||
// 终态状态集合
|
|
||||||
private static final List<String> TERMINAL_STATUSES = List.of(STATUS_CLOSED, STATUS_REJECTED, STATUS_CANCELLED);
|
|
||||||
// 子需求允许大需求关闭的状态集合
|
|
||||||
private static final List<String> CHILD_ALLOW_CLOSE_STATUSES = List.of(STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED);
|
|
||||||
// 允许删除的状态集合(实施中之前的状态)
|
// 允许删除的状态集合(实施中之前的状态)
|
||||||
private static final List<String> ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH);
|
private static final List<String> ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CLAIM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH);
|
||||||
// 父需求取消时,子需求允许的状态集合(仅已拒绝和已取消)
|
// 父需求取消时,子需求允许的状态集合(仅已拒绝和已取消)
|
||||||
private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(STATUS_REJECTED, STATUS_CANCELLED);
|
private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(STATUS_REJECTED, STATUS_CANCELLED);
|
||||||
|
|
||||||
@@ -76,6 +79,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
private static final String PRODUCT_QUERY_PERMISSION = "project:product:query";
|
private static final String PRODUCT_QUERY_PERMISSION = "project:product:query";
|
||||||
private static final String PRODUCT_UPDATE_PERMISSION = "project:product:update";
|
private static final String PRODUCT_UPDATE_PERMISSION = "project:product:update";
|
||||||
private static final String PRODUCT_STATUS_PERMISSION = "project:product:status";
|
private static final String PRODUCT_STATUS_PERMISSION = "project:product:status";
|
||||||
|
private static final String PRODUCT_REVIEW_PERMISSION = ProductObjectConstants.PERMISSION_REVIEW;
|
||||||
private static final String PRODUCT_DELETE_PERMISSION = "project:product:delete";
|
private static final String PRODUCT_DELETE_PERMISSION = "project:product:delete";
|
||||||
private static final String PRODUCT_SPLIT_PERMISSION = "project:product:split";
|
private static final String PRODUCT_SPLIT_PERMISSION = "project:product:split";
|
||||||
|
|
||||||
@@ -88,10 +92,18 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
private static final String ACTION_ACCEPT = "accept";
|
private static final String ACTION_ACCEPT = "accept";
|
||||||
private static final String ACTION_DISPATCH = "dispatch";
|
private static final String ACTION_DISPATCH = "dispatch";
|
||||||
private static final String ACTION_CANCEL = "cancel";
|
private static final String ACTION_CANCEL = "cancel";
|
||||||
|
private static final String ACTION_REJECT = "reject";
|
||||||
|
private static final String ACTION_PASS_REVIEW = "pass_review";
|
||||||
|
private static final String ACTION_REJECT_REVIEW = "reject_review";
|
||||||
private static final String ACTION_AUTO_DERIVE = "auto_derive";
|
private static final String ACTION_AUTO_DERIVE = "auto_derive";
|
||||||
|
|
||||||
private static final String BIZ_TYPE_REQUIREMENT = "product_requirement";
|
private static final String BIZ_TYPE_REQUIREMENT = "product_requirement";
|
||||||
private static final String AUTO_DERIVE_REASON = "根据子需求状态自动推导";
|
private static final String AUTO_DERIVE_REASON = "根据子需求状态自动推导";
|
||||||
|
private static final int DASHBOARD_RECENT_CHANGE_LIMIT = 5;
|
||||||
|
private static final String DASHBOARD_ACTION_STATUS_TERMINAL = "status_terminal";
|
||||||
|
private static final String DASHBOARD_LABEL_CREATE = "需求新增";
|
||||||
|
private static final String DASHBOARD_LABEL_DELETE = "需求删除";
|
||||||
|
private static final String DASHBOARD_LABEL_STATUS = "状态流转";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ProductRequirementMapper requirementMapper;
|
private ProductRequirementMapper requirementMapper;
|
||||||
@@ -113,6 +125,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
private UserObjectRoleMapper userObjectRoleMapper;
|
private UserObjectRoleMapper userObjectRoleMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||||
|
@Resource
|
||||||
|
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||||
|
|
||||||
// ========== 需求增删改查 ==========
|
// ========== 需求增删改查 ==========
|
||||||
|
|
||||||
@@ -136,7 +150,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
requirement.setTitle(createReqVO.getTitle().trim());
|
requirement.setTitle(createReqVO.getTitle().trim());
|
||||||
requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
|
requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
|
||||||
requirement.setCategory(createReqVO.getCategory());
|
requirement.setCategory(createReqVO.getCategory());
|
||||||
requirement.setSourceType("manual"); // 手工新增默认来源类型
|
requirement.setSourceType(SOURCE_TYPE_MANUAL); // 手工新增默认来源类型
|
||||||
requirement.setPriority(createReqVO.getPriority());
|
requirement.setPriority(createReqVO.getPriority());
|
||||||
// 根据是否需要评审确定初始状态
|
// 根据是否需要评审确定初始状态
|
||||||
String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
|
String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
|
||||||
@@ -164,7 +178,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
permission = PRODUCT_UPDATE_PERMISSION)
|
permission = PRODUCT_UPDATE_PERMISSION)
|
||||||
public void updateRequirement(ProductRequirementUpdateReqVO updateReqVO) {
|
public void updateRequirement(ProductRequirementUpdateReqVO updateReqVO) {
|
||||||
ProductRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
|
ProductRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
|
||||||
// 校验终态不允许编辑
|
// 校验当前状态是否允许编辑
|
||||||
validateRequirementEditable(requirement);
|
validateRequirementEditable(requirement);
|
||||||
// 当未选择模块时,自动归属到该产品的"全部需求"模块
|
// 当未选择模块时,自动归属到该产品的"全部需求"模块
|
||||||
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProductId());
|
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProductId());
|
||||||
@@ -292,6 +306,263 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
return new PageResult<>(list, (long) total);
|
return new PageResult<>(list, (long) total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||||
|
permission = PRODUCT_QUERY_PERMISSION)
|
||||||
|
public ProductRequirementDashboardRespVO getRequirementDashboard(Long productId) {
|
||||||
|
List<ProductRequirementDO> requirements = requirementMapper.selectListByProductId(productId);
|
||||||
|
Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();
|
||||||
|
|
||||||
|
ProductRequirementDashboardRespVO respVO = new ProductRequirementDashboardRespVO();
|
||||||
|
respVO.setSummary(buildRequirementDashboardSummary(requirements));
|
||||||
|
respVO.setRecentChanges(buildRequirementDashboardRecentChanges(productId, requirements, statusModelMap));
|
||||||
|
return respVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProductRequirementDashboardSummaryRespVO buildRequirementDashboardSummary(List<ProductRequirementDO> requirements) {
|
||||||
|
long total = requirements.size();
|
||||||
|
long pendingClaim = requirements.stream()
|
||||||
|
.filter(requirement -> STATUS_PENDING_CLAIM.equals(requirement.getStatusCode()))
|
||||||
|
.count();
|
||||||
|
long pendingReview = requirements.stream()
|
||||||
|
.filter(requirement -> STATUS_PENDING_REVIEW.equals(requirement.getStatusCode()))
|
||||||
|
.count();
|
||||||
|
long pendingDispatch = requirements.stream()
|
||||||
|
.filter(requirement -> isPendingDispatchActionStatus(requirement.getStatusCode()))
|
||||||
|
.count();
|
||||||
|
long todo = pendingClaim + pendingReview + pendingDispatch;
|
||||||
|
long completed = requirements.stream()
|
||||||
|
.filter(requirement -> STATUS_ACCEPTED.equals(requirement.getStatusCode())
|
||||||
|
|| STATUS_CLOSED.equals(requirement.getStatusCode()))
|
||||||
|
.count();
|
||||||
|
long highPriorityTodo = requirements.stream()
|
||||||
|
.filter(requirement -> isTodoStatus(requirement.getStatusCode()))
|
||||||
|
.filter(requirement -> requirement.getPriority() != null
|
||||||
|
&& (requirement.getPriority() == 0 || requirement.getPriority() == 1))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
ProductRequirementDashboardSummaryRespVO summary = new ProductRequirementDashboardSummaryRespVO();
|
||||||
|
summary.setTotal(total);
|
||||||
|
summary.setTodo(todo);
|
||||||
|
summary.setPendingClaim(pendingClaim);
|
||||||
|
summary.setPendingReview(pendingReview);
|
||||||
|
summary.setPendingDispatch(pendingDispatch);
|
||||||
|
summary.setCompleted(completed);
|
||||||
|
summary.setCompletionRate(total == 0 ? 0 : Math.toIntExact(Math.round(completed * 100.0 / total)));
|
||||||
|
summary.setHighPriorityTodo(highPriorityTodo);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ProductRequirementDashboardRecentChangeRespVO> buildRequirementDashboardRecentChanges(
|
||||||
|
Long productId, List<ProductRequirementDO> requirements, Map<String, ObjectStatusModelDO> statusModelMap) {
|
||||||
|
List<DashboardRecentChangeItem> items = new ArrayList<>();
|
||||||
|
appendRequirementAuditRecentChanges(productId, statusModelMap, items);
|
||||||
|
appendRequirementTerminalStatusRecentChanges(requirements, statusModelMap, items);
|
||||||
|
|
||||||
|
items.sort((left, right) -> {
|
||||||
|
int timeCompare = compareNullableLocalDateTimeDesc(left.occurredAt(), right.occurredAt());
|
||||||
|
if (timeCompare != 0) {
|
||||||
|
return timeCompare;
|
||||||
|
}
|
||||||
|
return compareNullableLongDesc(left.sourceId(), right.sourceId());
|
||||||
|
});
|
||||||
|
|
||||||
|
return items.stream()
|
||||||
|
.limit(DASHBOARD_RECENT_CHANGE_LIMIT)
|
||||||
|
.map(DashboardRecentChangeItem::respVO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compareNullableLocalDateTimeDesc(LocalDateTime left, LocalDateTime right) {
|
||||||
|
if (left == null && right == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (left == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (right == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return right.compareTo(left);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compareNullableLongDesc(Long left, Long right) {
|
||||||
|
if (left == null && right == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (left == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (right == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return right.compareTo(left);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendRequirementAuditRecentChanges(Long productId, Map<String, ObjectStatusModelDO> statusModelMap,
|
||||||
|
List<DashboardRecentChangeItem> items) {
|
||||||
|
List<BizAuditLogDO> logs = bizAuditLogMapper.selectListByBizTypeAndActions(
|
||||||
|
BIZ_TYPE_REQUIREMENT, List.of(ACTION_CREATE, ACTION_DELETE), null, null);
|
||||||
|
for (BizAuditLogDO log : logs) {
|
||||||
|
if (ACTION_CREATE.equals(log.getActionType())) {
|
||||||
|
appendRequirementCreateRecentChange(productId, statusModelMap, items, log);
|
||||||
|
} else if (ACTION_DELETE.equals(log.getActionType())) {
|
||||||
|
appendRequirementDeleteRecentChange(productId, items, log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendRequirementCreateRecentChange(Long productId, Map<String, ObjectStatusModelDO> statusModelMap,
|
||||||
|
List<DashboardRecentChangeItem> items, BizAuditLogDO log) {
|
||||||
|
Long logProductId = getFieldChangeLong(log.getFieldChanges(), "productId", "after");
|
||||||
|
if (!Objects.equals(logProductId, productId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String title = getFieldChangeString(log.getFieldChanges(), "title", "after");
|
||||||
|
String statusCode = getFieldChangeString(log.getFieldChanges(), "statusCode", "after");
|
||||||
|
ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
|
||||||
|
"requirement:create:" + log.getId(),
|
||||||
|
log.getBizId(),
|
||||||
|
defaultDashboardTitle(title),
|
||||||
|
ACTION_CREATE,
|
||||||
|
DASHBOARD_LABEL_CREATE,
|
||||||
|
"当前状态:" + resolveStatusName(statusModelMap, statusCode),
|
||||||
|
log.getCreateTime(),
|
||||||
|
log.getOperatorUserId(),
|
||||||
|
log.getOperatorName());
|
||||||
|
items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendRequirementDeleteRecentChange(Long productId, List<DashboardRecentChangeItem> items,
|
||||||
|
BizAuditLogDO log) {
|
||||||
|
Long logProductId = getFieldChangeLong(log.getFieldChanges(), "productId", "before");
|
||||||
|
if (!Objects.equals(logProductId, productId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String title = getFieldChangeString(log.getFieldChanges(), "title", "before");
|
||||||
|
ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
|
||||||
|
"requirement:delete:" + log.getId(),
|
||||||
|
log.getBizId(),
|
||||||
|
defaultDashboardTitle(title),
|
||||||
|
ACTION_DELETE,
|
||||||
|
DASHBOARD_LABEL_DELETE,
|
||||||
|
"该需求已被删除",
|
||||||
|
log.getCreateTime(),
|
||||||
|
log.getOperatorUserId(),
|
||||||
|
log.getOperatorName());
|
||||||
|
items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendRequirementTerminalStatusRecentChanges(List<ProductRequirementDO> requirements,
|
||||||
|
Map<String, ObjectStatusModelDO> statusModelMap,
|
||||||
|
List<DashboardRecentChangeItem> items) {
|
||||||
|
List<Long> requirementIds = requirements.stream()
|
||||||
|
.map(ProductRequirementDO::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<String> terminalStatusCodes = statusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE);
|
||||||
|
List<ProductRequirementStatusLogDO> logs = statusLogMapper
|
||||||
|
.selectListByRequirementIdsAndToStatuses(requirementIds, terminalStatusCodes);
|
||||||
|
for (ProductRequirementStatusLogDO log : logs) {
|
||||||
|
String statusName = resolveStatusName(statusModelMap, log.getToStatus());
|
||||||
|
ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
|
||||||
|
"requirement:status:" + log.getId(),
|
||||||
|
log.getRequirementId(),
|
||||||
|
defaultDashboardTitle(log.getRequirementTitleSnapshot()),
|
||||||
|
DASHBOARD_ACTION_STATUS_TERMINAL,
|
||||||
|
DASHBOARD_LABEL_STATUS,
|
||||||
|
"流转至终止态:" + statusName,
|
||||||
|
log.getCreateTime(),
|
||||||
|
log.getOperatorUserId(),
|
||||||
|
log.getOperatorName());
|
||||||
|
items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProductRequirementDashboardRecentChangeRespVO buildDashboardRecentChange(
|
||||||
|
String id, Long requirementId, String title, String actionType, String actionLabel,
|
||||||
|
String content, LocalDateTime occurredAt, Long operatorUserId, String operatorName) {
|
||||||
|
ProductRequirementDashboardRecentChangeRespVO respVO = new ProductRequirementDashboardRecentChangeRespVO();
|
||||||
|
respVO.setId(id);
|
||||||
|
respVO.setRequirementId(requirementId);
|
||||||
|
respVO.setTitle(title);
|
||||||
|
respVO.setActionType(actionType);
|
||||||
|
respVO.setActionLabel(actionLabel);
|
||||||
|
respVO.setContent(content);
|
||||||
|
respVO.setOccurredAt(occurredAt);
|
||||||
|
respVO.setOperatorUserId(operatorUserId);
|
||||||
|
respVO.setOperatorName(operatorName);
|
||||||
|
return respVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveStatusName(Map<String, ObjectStatusModelDO> statusModelMap, String statusCode) {
|
||||||
|
if (!StringUtils.hasText(statusCode)) {
|
||||||
|
return "未知状态";
|
||||||
|
}
|
||||||
|
ObjectStatusModelDO statusModel = statusModelMap.get(statusCode);
|
||||||
|
return statusModel != null && StringUtils.hasText(statusModel.getStatusName())
|
||||||
|
? statusModel.getStatusName() : statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultDashboardTitle(String title) {
|
||||||
|
return StringUtils.hasText(title) ? title : "未命名需求";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTodoStatus(String statusCode) {
|
||||||
|
return STATUS_PENDING_CLAIM.equals(statusCode)
|
||||||
|
|| STATUS_PENDING_REVIEW.equals(statusCode)
|
||||||
|
|| isPendingDispatchActionStatus(statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品对象域的概览首页“待指派”表示等待执行指派动作,不等同于单一 pending_dispatch 状态。
|
||||||
|
*/
|
||||||
|
private boolean isPendingDispatchActionStatus(String statusCode) {
|
||||||
|
return STATUS_PENDING_DISPATCH.equals(statusCode) || STATUS_REVIEWED.equals(statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getFieldChangeLong(String fieldChanges, String fieldName, String valueField) {
|
||||||
|
JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
|
||||||
|
if (valueNode == null || valueNode.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (valueNode.isNumber()) {
|
||||||
|
return valueNode.longValue();
|
||||||
|
}
|
||||||
|
if (valueNode.isTextual() && StringUtils.hasText(valueNode.textValue())) {
|
||||||
|
return Long.valueOf(valueNode.textValue().trim());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFieldChangeString(String fieldChanges, String fieldName, String valueField) {
|
||||||
|
JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
|
||||||
|
if (valueNode == null || valueNode.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (valueNode.isTextual()) {
|
||||||
|
return valueNode.textValue();
|
||||||
|
}
|
||||||
|
return valueNode.asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode getFieldChangeNode(String fieldChanges, String fieldName, String valueField) {
|
||||||
|
if (!StringUtils.hasText(fieldChanges) || !JsonUtils.isJsonObject(fieldChanges)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode fieldNode = JsonUtils.parseTree(fieldChanges).path(fieldName);
|
||||||
|
if (fieldNode.isMissingNode()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode valueNode = fieldNode.path(valueField);
|
||||||
|
return valueNode.isMissingNode() ? null : valueNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DashboardRecentChangeItem(Long sourceId, LocalDateTime occurredAt,
|
||||||
|
ProductRequirementDashboardRecentChangeRespVO respVO) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 向上追溯需求的根节点ID,同时收集路径上的所有节点ID
|
* 向上追溯需求的根节点ID,同时收集路径上的所有节点ID
|
||||||
*
|
*
|
||||||
@@ -471,6 +742,19 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||||
permission = PRODUCT_STATUS_PERMISSION)
|
permission = PRODUCT_STATUS_PERMISSION)
|
||||||
public void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
|
public void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
|
||||||
|
doChangeRequirementStatus(reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||||
|
permission = PRODUCT_REVIEW_PERMISSION)
|
||||||
|
public void changeRequirementStatusForReview(ProductRequirementStatusActionReqVO reqVO) {
|
||||||
|
validateReviewStatusAction(reqVO.getActionCode());
|
||||||
|
doChangeRequirementStatus(reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doChangeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
|
||||||
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
||||||
String actionCode = reqVO.getActionCode().trim();
|
String actionCode = reqVO.getActionCode().trim();
|
||||||
Long implementProjectId = reqVO.getImplementProjectId();
|
Long implementProjectId = reqVO.getImplementProjectId();
|
||||||
@@ -479,6 +763,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
|
|
||||||
// 校验状态流转是否合法
|
// 校验状态流转是否合法
|
||||||
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
||||||
|
validateReviewRejectedActionAllowed(requirement, actionCode);
|
||||||
String reason = normalizeNullableText(reqVO.getReason());
|
String reason = normalizeNullableText(reqVO.getReason());
|
||||||
// 校验是否需要填写原因
|
// 校验是否需要填写原因
|
||||||
validateTransitionReason(transition, reason);
|
validateTransitionReason(transition, reason);
|
||||||
@@ -497,14 +782,14 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
if (ACTION_CLOSE.equals(actionCode)) {
|
if (ACTION_CLOSE.equals(actionCode)) {
|
||||||
closeAllAcceptedChildren(reqVO.getId(), reason);
|
closeAllAcceptedChildren(reqVO.getId(), reason);
|
||||||
}
|
}
|
||||||
// dispatch动作且选择了关联项目时,校验负责人是否在项目中,然后自动创建对应的项目需求
|
// dispatch动作且选择了关联项目时,校验负责人是否在产品团队中,然后自动创建对应的项目需求
|
||||||
if (ACTION_DISPATCH.equals(actionCode) && implementProjectId != null) {
|
if (ACTION_DISPATCH.equals(actionCode) && implementProjectId != null) {
|
||||||
// 校验负责人是否为目标项目的成员
|
// 校验负责人是否为所属产品的团队成员
|
||||||
if (requirement.getCurrentHandlerUserId() != null) {
|
if (requirement.getCurrentHandlerUserId() != null) {
|
||||||
List<UserObjectRoleDO> userObjectRoleDOS = userObjectRoleMapper.selectActiveListByObjectAndUserId(
|
List<UserObjectRoleDO> userObjectRoleDOS = userObjectRoleMapper.selectActiveListByObjectAndUserId(
|
||||||
ProjectObjectConstants.OBJECT_TYPE, implementProjectId, requirement.getCurrentHandlerUserId());
|
ProductObjectConstants.OBJECT_TYPE, requirement.getProductId(), requirement.getCurrentHandlerUserId());
|
||||||
if (userObjectRoleDOS.isEmpty()) {
|
if (userObjectRoleDOS.isEmpty()) {
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_HANDLER_NOT_PROJECT_MEMBER);
|
throw exception(ErrorCodeConstants.REQUIREMENT_HANDLER_NOT_PRODUCT_MEMBER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createProjectRequirementFromProduct(requirement, implementProjectId);
|
createProjectRequirementFromProduct(requirement, implementProjectId);
|
||||||
@@ -528,16 +813,24 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
refreshAncestorStatusRecursively(requirement.getId());
|
refreshAncestorStatusRecursively(requirement.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateReviewStatusAction(String actionCode) {
|
||||||
|
String normalizedActionCode = actionCode == null ? null : actionCode.trim();
|
||||||
|
if (!ACTION_PASS_REVIEW.equals(normalizedActionCode) && !ACTION_REJECT_REVIEW.equals(normalizedActionCode)) {
|
||||||
|
throw invalidParamException("评审权限只能触发评审通过或评审不通过动作");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验需求的所有子需求(包括子子需求)是否处于允许关闭或验收的状态
|
* 校验需求的所有子需求(包括子子需求)是否处于允许关闭或验收的状态
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
|
void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
|
||||||
List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
||||||
|
Set<String> terminalStatusCodes = getTerminalStatusCodes();
|
||||||
// 排除自身,只校验子需求
|
// 排除自身,只校验子需求
|
||||||
for (ProductRequirementDO req : allChildren) {
|
for (ProductRequirementDO req : allChildren) {
|
||||||
if (!Objects.equals(req.getId(), requirementId)) {
|
if (!Objects.equals(req.getId(), requirementId)) {
|
||||||
if (!CHILD_ALLOW_CLOSE_STATUSES.contains(req.getStatusCode())) {
|
if (!STATUS_ACCEPTED.equals(req.getStatusCode()) && !terminalStatusCodes.contains(req.getStatusCode())) {
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
|
throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,7 +868,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
throw exception(ErrorCodeConstants.REQUIREMENT_HAS_CHILDREN);
|
throw exception(ErrorCodeConstants.REQUIREMENT_HAS_CHILDREN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验状态是否允许删除(只有待确认、待评审、待分流状态才能删除)
|
// 校验状态是否允许删除(只有待认领、待评审、待指派状态才能删除)
|
||||||
if (!ALLOW_DELETE_STATUSES.contains(fromStatus)) {
|
if (!ALLOW_DELETE_STATUSES.contains(fromStatus)) {
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_DELETE);
|
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_DELETE);
|
||||||
}
|
}
|
||||||
@@ -597,9 +890,9 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
public Long splitRequirement(ProductRequirementSplitReqVO reqVO) {
|
public Long splitRequirement(ProductRequirementSplitReqVO reqVO) {
|
||||||
// 校验父需求是否存在
|
// 校验父需求是否存在
|
||||||
ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId());
|
ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId());
|
||||||
// 产品需求一旦已分流生成项目需求,就只能到项目需求侧继续拆分
|
// 产品需求一旦已指派生成项目需求,就只能到项目需求侧继续拆分
|
||||||
validateRequirementNotDispatched(parentRequirement);
|
validateRequirementNotDispatched(parentRequirement);
|
||||||
// 校验父需求状态是否允许拆分(只能是待分流或实施中)
|
// 校验父需求状态是否允许拆分(只能是待指派、已评审或实施中)
|
||||||
validateParentAllowSplit(parentRequirement);
|
validateParentAllowSplit(parentRequirement);
|
||||||
AttachmentValidator.validate(reqVO.getAttachments());
|
AttachmentValidator.validate(reqVO.getAttachments());
|
||||||
attachmentFileIdResolver.resolve(reqVO.getAttachments());
|
attachmentFileIdResolver.resolve(reqVO.getAttachments());
|
||||||
@@ -615,7 +908,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
childRequirement.setCategory(reqVO.getCategory());
|
childRequirement.setCategory(reqVO.getCategory());
|
||||||
childRequirement.setSourceType(parentRequirement.getSourceType()); // 继承父需求来源类型
|
childRequirement.setSourceType(parentRequirement.getSourceType()); // 继承父需求来源类型
|
||||||
childRequirement.setPriority(reqVO.getPriority());
|
childRequirement.setPriority(reqVO.getPriority());
|
||||||
// 子需求初始状态为待分流
|
// 子需求初始状态为待指派
|
||||||
// 根据是否需要评审确定初始状态
|
// 根据是否需要评审确定初始状态
|
||||||
String initialStatus = Objects.equals(reqVO.getReviewRequired(), 1)
|
String initialStatus = Objects.equals(reqVO.getReviewRequired(), 1)
|
||||||
? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
|
? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
|
||||||
@@ -630,8 +923,8 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
childRequirement.setAttachments(reqVO.getAttachments());
|
childRequirement.setAttachments(reqVO.getAttachments());
|
||||||
requirementMapper.insert(childRequirement);
|
requirementMapper.insert(childRequirement);
|
||||||
|
|
||||||
// 父需求状态从待分流变为实施中
|
// 父需求等待执行指派动作时,一旦拆分子需求就进入实施中。
|
||||||
if (STATUS_PENDING_DISPATCH.equals(parentRequirement.getStatusCode())) {
|
if (isPendingDispatchActionStatus(parentRequirement.getStatusCode())) {
|
||||||
ProductRequirementDO before = cloneRequirement(parentRequirement);
|
ProductRequirementDO before = cloneRequirement(parentRequirement);
|
||||||
String fromStatus = parentRequirement.getStatusCode();
|
String fromStatus = parentRequirement.getStatusCode();
|
||||||
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||||
@@ -715,7 +1008,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
permission = PRODUCT_QUERY_PERMISSION)
|
permission = PRODUCT_QUERY_PERMISSION)
|
||||||
public List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId) {
|
public List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId) {
|
||||||
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
||||||
// 当产品需求已分流并生成项目需求后,产品需求端不再返回动作按钮
|
// 当产品需求已指派并生成项目需求后,产品需求端不再返回动作按钮
|
||||||
if (hasDispatchedProjectRequirement(requirement)) {
|
if (hasDispatchedProjectRequirement(requirement)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
@@ -787,50 +1080,17 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 该方法作用和getAllowedTransitions()类似,是用来获取当前状态下可以进行的动作
|
|
||||||
*
|
|
||||||
* @param requirementId 需求编号
|
|
||||||
* @param productId 产品编号
|
|
||||||
* @return ProductRequirementLifecycleRespVO
|
|
||||||
* @deprecated 产品需求页面最开始用来下拉框改状态时使用的,已经弃用
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Deprecated
|
|
||||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
|
||||||
permission = PRODUCT_QUERY_PERMISSION)
|
|
||||||
public ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId) {
|
|
||||||
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
|
||||||
String currentStatus = requirement.getStatusCode();
|
|
||||||
|
|
||||||
// 查询当前状态定义
|
|
||||||
ObjectStatusModelDO statusModel = statusModelMapper
|
|
||||||
.selectByObjectTypeAndStatusCodeEnabled(REQUIREMENT_OBJECT_TYPE, currentStatus);
|
|
||||||
if (statusModel == null) {
|
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProductRequirementLifecycleRespVO lifecycle = new ProductRequirementLifecycleRespVO();
|
|
||||||
lifecycle.setStatusCode(statusModel.getStatusCode());
|
|
||||||
lifecycle.setStatusName(statusModel.getStatusName());
|
|
||||||
lifecycle.setTerminal(statusModel.getTerminalFlag());
|
|
||||||
lifecycle.setAllowEdit(statusModel.getAllowEdit());
|
|
||||||
lifecycle.setLastStatusReason(requirement.getLastStatusReason());
|
|
||||||
lifecycle.setAvailableActions(getAllowedTransitions(requirementId, productId));
|
|
||||||
|
|
||||||
return lifecycle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId) {
|
public ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId) {
|
||||||
// 校验产品需求是否存在,以及是否已分流到具体的关联项目
|
// 校验产品需求是否存在,以及是否已指派到具体的关联项目
|
||||||
ProductRequirementDO requirement = validateRequirementExists(productRequirementId);
|
ProductRequirementDO requirement = validateRequirementExists(productRequirementId);
|
||||||
if (requirement.getImplementProjectId() == null) {
|
if (requirement.getImplementProjectId() == null) {
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_NOT_DISPATCHED);
|
throw exception(ErrorCodeConstants.REQUIREMENT_NOT_DISPATCHED);
|
||||||
}
|
}
|
||||||
Long projectId = requirement.getImplementProjectId();
|
Long projectId = requirement.getImplementProjectId();
|
||||||
|
|
||||||
// 查询产品需求分流后生成的顶级项目需求
|
// 查询产品需求指派后生成的顶级项目需求
|
||||||
List<ProjectRequirementDO> projectRequirements = projectRequirementMapper.selectList(
|
List<ProjectRequirementDO> projectRequirements = projectRequirementMapper.selectList(
|
||||||
new LambdaQueryWrapperX<ProjectRequirementDO>()
|
new LambdaQueryWrapperX<ProjectRequirementDO>()
|
||||||
.eq(ProjectRequirementDO::getProductRequirementId, productRequirementId)
|
.eq(ProjectRequirementDO::getProductRequirementId, productRequirementId)
|
||||||
@@ -954,6 +1214,9 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
* 当前只对取消动作做额外过滤,避免前端展示点了也会报错的按钮。
|
* 当前只对取消动作做额外过滤,避免前端展示点了也会报错的按钮。
|
||||||
*/
|
*/
|
||||||
private boolean shouldExposeTransition(ProductRequirementDO requirement, ObjectStatusTransitionDO transition) {
|
private boolean shouldExposeTransition(ProductRequirementDO requirement, ObjectStatusTransitionDO transition) {
|
||||||
|
if (!isReviewRejectedActionAllowed(requirement, transition.getActionCode())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!ACTION_CANCEL.equals(transition.getActionCode())) {
|
if (!ACTION_CANCEL.equals(transition.getActionCode())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -963,6 +1226,30 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
return isParentCancelAllowed(requirement.getId());
|
return isParentCancelAllowed(requirement.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评审不通过后的动作由来源决定:手工新增只能取消,工单流转只能拒绝。
|
||||||
|
*/
|
||||||
|
private void validateReviewRejectedActionAllowed(ProductRequirementDO requirement, String actionCode) {
|
||||||
|
if (!isReviewRejectedActionAllowed(requirement, actionCode)) {
|
||||||
|
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode()),
|
||||||
|
statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isReviewRejectedActionAllowed(ProductRequirementDO requirement, String actionCode) {
|
||||||
|
if (!STATUS_REVIEW_REJECTED.equals(requirement.getStatusCode())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (SOURCE_TYPE_MANUAL.equals(requirement.getSourceType())) {
|
||||||
|
return ACTION_CANCEL.equals(actionCode);
|
||||||
|
}
|
||||||
|
if (SOURCE_TYPE_WORK_ORDER.equals(requirement.getSourceType())) {
|
||||||
|
return ACTION_REJECT.equals(actionCode);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 父需求存在子需求时,只有全部子需求都已取消或已拒绝,才允许展示取消动作。
|
* 父需求存在子需求时,只有全部子需求都已取消或已拒绝,才允许展示取消动作。
|
||||||
*/
|
*/
|
||||||
@@ -1156,6 +1443,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
respVO.setSort(statusModel.getSort());
|
respVO.setSort(statusModel.getSort());
|
||||||
respVO.setInitialFlag(statusModel.getInitialFlag());
|
respVO.setInitialFlag(statusModel.getInitialFlag());
|
||||||
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
||||||
|
respVO.setAllowEdit(statusModel.getAllowEdit());
|
||||||
return respVO;
|
return respVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1177,11 +1465,6 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
ObjectStatusModelDO statusModel = statusModelMap.get(requirement.getStatusCode());
|
ObjectStatusModelDO statusModel = statusModelMap.get(requirement.getStatusCode());
|
||||||
if (statusModel != null) {
|
if (statusModel != null) {
|
||||||
respVO.setStatusName(statusModel.getStatusName());
|
respVO.setStatusName(statusModel.getStatusName());
|
||||||
respVO.setTerminal(statusModel.getTerminalFlag());
|
|
||||||
}
|
|
||||||
// 设置是否终态
|
|
||||||
if (respVO.getTerminal() == null) {
|
|
||||||
respVO.setTerminal(TERMINAL_STATUSES.contains(requirement.getStatusCode()));
|
|
||||||
}
|
}
|
||||||
return respVO;
|
return respVO;
|
||||||
}
|
}
|
||||||
@@ -1216,22 +1499,32 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验需求是否允许编辑(终态不允许编辑)
|
* 校验需求是否允许编辑,编辑能力以状态模型 allow_edit 为准。
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateRequirementEditable(ProductRequirementDO requirement) {
|
void validateRequirementEditable(ProductRequirementDO requirement) {
|
||||||
if (TERMINAL_STATUSES.contains(requirement.getStatusCode())) {
|
ObjectStatusModelDO statusModel = statusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||||
|
REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode());
|
||||||
|
if (statusModel == null) {
|
||||||
|
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
|
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<String> getTerminalStatusCodes() {
|
||||||
|
return new HashSet<>(statusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验父需求是否允许拆分
|
* 校验父需求是否允许拆分
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateParentAllowSplit(ProductRequirementDO parentRequirement) {
|
void validateParentAllowSplit(ProductRequirementDO parentRequirement) {
|
||||||
String status = parentRequirement.getStatusCode();
|
String status = parentRequirement.getStatusCode();
|
||||||
if (!STATUS_PENDING_DISPATCH.equals(status) && !STATUS_IMPLEMENTING.equals(status)) {
|
if (!STATUS_PENDING_DISPATCH.equals(status) && !STATUS_IMPLEMENTING.equals(status)
|
||||||
|
&& !STATUS_REVIEWED.equals(status)) {
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT);
|
throw exception(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1244,7 +1537,9 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
ObjectStatusTransitionDO transition = statusTransitionMapper
|
ObjectStatusTransitionDO transition = statusTransitionMapper
|
||||||
.selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode);
|
.selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode);
|
||||||
if (transition == null) {
|
if (transition == null) {
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, fromStatusCode),
|
||||||
|
statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
|
||||||
}
|
}
|
||||||
return transition;
|
return transition;
|
||||||
}
|
}
|
||||||
@@ -1255,7 +1550,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
||||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1494,7 +1789,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
// 查询关联项目下的根模块(parentId = 0)
|
// 查询关联项目下的根模块(parentId = 0)
|
||||||
ProjectRequirementModuleDO rootModule = projectRequirementModuleMapper.selectByProjectIdAndParentId(implementProjectId, 0L);
|
ProjectRequirementModuleDO rootModule = projectRequirementModuleMapper.selectByProjectIdAndParentId(implementProjectId, 0L);
|
||||||
if (rootModule == null) {
|
if (rootModule == null) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_ROOT_NOT_EXISTS);
|
throw exception(ErrorCodeConstants.REQUIREMENT_PROJECT_MODULE_ROOT_NOT_EXISTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建项目需求记录
|
// 构建项目需求记录
|
||||||
@@ -1506,7 +1801,7 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
newRequirement.setSourceType("product_requirement");
|
newRequirement.setSourceType("product_requirement");
|
||||||
newRequirement.setStatusCode(STATUS_IMPLEMENTING);
|
newRequirement.setStatusCode(STATUS_IMPLEMENTING);
|
||||||
newRequirement.setReviewRequired(0); //从产品需求流转到项目需求的需求肯定不需要评审
|
newRequirement.setReviewRequired(0); //从产品需求流转到项目需求的需求肯定不需要评审
|
||||||
// 拷贝产品需求的其他字段(不拷贝排序、状态原因、更新人、更新时间、逻辑删除字段)
|
// 拷贝产品需求的其他字段(不拷贝需求负责人id和姓名、排序、状态原因、更新人、更新时间、逻辑删除字段)
|
||||||
newRequirement.setTitle(productRequirement.getTitle());
|
newRequirement.setTitle(productRequirement.getTitle());
|
||||||
newRequirement.setDescription(productRequirement.getDescription());
|
newRequirement.setDescription(productRequirement.getDescription());
|
||||||
newRequirement.setCategory(productRequirement.getCategory());
|
newRequirement.setCategory(productRequirement.getCategory());
|
||||||
@@ -1515,8 +1810,6 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
newRequirement.setProposerId(productRequirement.getProposerId());
|
newRequirement.setProposerId(productRequirement.getProposerId());
|
||||||
newRequirement.setProposerNickname(productRequirement.getProposerNickname());
|
newRequirement.setProposerNickname(productRequirement.getProposerNickname());
|
||||||
newRequirement.setExpectedTime(productRequirement.getExpectedTime());
|
newRequirement.setExpectedTime(productRequirement.getExpectedTime());
|
||||||
newRequirement.setCurrentHandlerUserId(productRequirement.getCurrentHandlerUserId());
|
|
||||||
newRequirement.setCurrentHandlerUserNickname(productRequirement.getCurrentHandlerUserNickname());
|
|
||||||
newRequirement.setAttachments(productRequirement.getAttachments());
|
newRequirement.setAttachments(productRequirement.getAttachments());
|
||||||
newRequirement.setCreator(productRequirement.getCreator());
|
newRequirement.setCreator(productRequirement.getCreator());
|
||||||
newRequirement.setCreateTime(productRequirement.getCreateTime());
|
newRequirement.setCreateTime(productRequirement.getCreateTime());
|
||||||
@@ -1525,3 +1818,4 @@ public class ProductRequirementServiceImpl implements ProductRequirementService
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ public class ProductServiceImpl implements ProductService {
|
|||||||
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
||||||
@Resource
|
@Resource
|
||||||
private ObjectDataScopeService objectDataScopeService;
|
private ObjectDataScopeService objectDataScopeService;
|
||||||
|
@Resource
|
||||||
|
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -274,7 +276,7 @@ public class ProductServiceImpl implements ProductService {
|
|||||||
// 显式角色为空:走 scope.contains 判定隐式 observer 兜底(设计文档 2.1 节末段)
|
// 显式角色为空:走 scope.contains 判定隐式 observer 兜底(设计文档 2.1 节末段)
|
||||||
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
|
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
|
||||||
if (!scope.contains(id, product.getDirectionCode())) {
|
if (!scope.contains(id, product.getDirectionCode())) {
|
||||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED, "查看");
|
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
|
||||||
}
|
}
|
||||||
return buildImplicitObserverContext(product);
|
return buildImplicitObserverContext(product);
|
||||||
}
|
}
|
||||||
@@ -568,7 +570,9 @@ public class ProductServiceImpl implements ProductService {
|
|||||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||||
.selectByObjectTypeAndFromStatusAndAction(ProductObjectConstants.OBJECT_TYPE, fromStatusCode, actionCode);
|
.selectByObjectTypeAndFromStatusAndAction(ProductObjectConstants.OBJECT_TYPE, fromStatusCode, actionCode);
|
||||||
if (transition == null) {
|
if (transition == null) {
|
||||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(ProductObjectConstants.OBJECT_TYPE, fromStatusCode),
|
||||||
|
statusActionTextResolver.actionName(ProductObjectConstants.OBJECT_TYPE, actionCode));
|
||||||
}
|
}
|
||||||
return transition;
|
return transition;
|
||||||
}
|
}
|
||||||
@@ -576,7 +580,7 @@ public class ProductServiceImpl implements ProductService {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
||||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductS
|
|||||||
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingRespVO;
|
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingRespVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
|
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -34,6 +36,7 @@ public class ProductSettingServiceImpl implements ProductSettingService {
|
|||||||
private ProductActivityTimelineQueryService productActivityTimelineQueryService;
|
private ProductActivityTimelineQueryService productActivityTimelineQueryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", accessible = true)
|
||||||
public ProductSettingRespVO getProductSettings(Long productId) {
|
public ProductSettingRespVO getProductSettings(Long productId) {
|
||||||
ProductDO product = validateProductExists(productId);
|
ProductDO product = validateProductExists(productId);
|
||||||
ProductSettingRespVO respVO = new ProductSettingRespVO();
|
ProductSettingRespVO respVO = new ProductSettingRespVO();
|
||||||
@@ -43,12 +46,14 @@ public class ProductSettingServiceImpl implements ProductSettingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", accessible = true)
|
||||||
public PageResult<ProductActivityRespVO> getProductActivities(Long productId, ProductActivityPageReqVO reqVO) {
|
public PageResult<ProductActivityRespVO> getProductActivities(Long productId, ProductActivityPageReqVO reqVO) {
|
||||||
validateProductExists(productId);
|
validateProductExists(productId);
|
||||||
return productActivityQueryService.getProductActivities(productId, reqVO);
|
return productActivityQueryService.getProductActivities(productId, reqVO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", accessible = true)
|
||||||
public PageResult<ProductActivityTimelineRespVO> getProductActivityTimelinePage(
|
public PageResult<ProductActivityTimelineRespVO> getProductActivityTimelinePage(
|
||||||
Long productId, ProductActivityTimelinePageReqVO reqVO) {
|
Long productId, ProductActivityTimelinePageReqVO reqVO) {
|
||||||
validateProductExists(productId);
|
validateProductExists(productId);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.njcn.rdms.module.project.service.project;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectOwnedRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectParticipatedRespVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台「我的项目」Service:按登录用户隐式聚合,无权限注解。
|
||||||
|
*/
|
||||||
|
public interface MyProjectService {
|
||||||
|
|
||||||
|
/** 我参与的项目(作为成员) */
|
||||||
|
PageResult<MyProjectParticipatedRespVO> getMyParticipatedPage(MyProjectPageReqVO reqVO);
|
||||||
|
|
||||||
|
/** 我负责的项目(managerUserId = 登录用户) */
|
||||||
|
PageResult<MyProjectOwnedRespVO> getMyOwnedPage(MyProjectPageReqVO reqVO);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
package com.njcn.rdms.module.project.service.project;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectOwnedRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.project.vo.myproject.MyProjectParticipatedRespVO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
|
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||||
|
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
|
||||||
|
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||||
|
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MyProjectServiceImpl implements MyProjectService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ProjectMapper projectMapper;
|
||||||
|
@Resource
|
||||||
|
private ProjectTaskMapper projectTaskMapper;
|
||||||
|
@Resource
|
||||||
|
private ProjectExecutionMapper projectExecutionMapper;
|
||||||
|
@Resource
|
||||||
|
private UserObjectRoleMapper userObjectRoleMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectPermissionApi objectPermissionApi;
|
||||||
|
@Resource
|
||||||
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
|
/** 工作台「我的项目」列表统一排序:按项目创建时间升序(先创建的在前),id 兜底保证稳定。 */
|
||||||
|
private static final Comparator<ProjectDO> PROJECT_CREATE_TIME_ASC =
|
||||||
|
Comparator.comparing(ProjectDO::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||||
|
.thenComparing(ProjectDO::getId);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<MyProjectParticipatedRespVO> getMyParticipatedPage(MyProjectPageReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
// 1. 我参与的所有 active 角色行(含 manager/dev 等多角色,objectId=项目id)
|
||||||
|
List<UserObjectRoleDO> myRoles = userObjectRoleMapper
|
||||||
|
.selectActiveListByObjectTypeAndUserId(ProjectObjectConstants.OBJECT_TYPE, loginUserId);
|
||||||
|
if (myRoles.isEmpty()) {
|
||||||
|
return new PageResult<>(Collections.emptyList(), 0L);
|
||||||
|
}
|
||||||
|
// 2. 按项目分组我的角色行
|
||||||
|
Map<Long, List<UserObjectRoleDO>> rolesByProject = myRoles.stream()
|
||||||
|
.filter(r -> r.getObjectId() != null)
|
||||||
|
.collect(Collectors.groupingBy(UserObjectRoleDO::getObjectId, LinkedHashMap::new, Collectors.toList()));
|
||||||
|
Set<Long> projectIds = new LinkedHashSet<>(rolesByProject.keySet());
|
||||||
|
// 3. 项目基本信息
|
||||||
|
List<ProjectDO> projects = projectMapper.selectBatchIds(projectIds);
|
||||||
|
if (projects.isEmpty()) {
|
||||||
|
return new PageResult<>(Collections.emptyList(), 0L);
|
||||||
|
}
|
||||||
|
// 3.1 仅保留非终态项目(终态项目不在工作台「我的项目」体现),并按创建时间升序(先创建的在前)
|
||||||
|
List<String> projectTerminal = objectStatusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE);
|
||||||
|
projects = projects.stream()
|
||||||
|
.filter(p -> !projectTerminal.contains(p.getStatusCode()))
|
||||||
|
.sorted(PROJECT_CREATE_TIME_ASC)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (projects.isEmpty()) {
|
||||||
|
return new PageResult<>(Collections.emptyList(), 0L);
|
||||||
|
}
|
||||||
|
// 4. statusName 批量回填
|
||||||
|
Map<String, String> statusNameMap = loadStatusNameMap(ProjectObjectConstants.OBJECT_TYPE);
|
||||||
|
// 5. 角色名 map(一次性拉全部涉及 roleId)
|
||||||
|
Map<Long, ObjectRoleRespDTO> roleMap = loadRoleMap(myRoles.stream()
|
||||||
|
.map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet()));
|
||||||
|
// 5.1 每个项目下"我的可见角色行":剔除 visible=0 的隐式角色(创建者 / 隐式观察者等业务自动赋予角色)。
|
||||||
|
// 若某项目下我没有任何可见角色,则不算"我参与的项目",整项剔除——与 ProjectMemberServiceImpl 团队列表口径一致。
|
||||||
|
Map<Long, List<UserObjectRoleDO>> visibleRolesByProject = new LinkedHashMap<>();
|
||||||
|
rolesByProject.forEach((pid, rows) -> {
|
||||||
|
List<UserObjectRoleDO> visible = filterVisibleRoleRows(rows, roleMap);
|
||||||
|
if (!visible.isEmpty()) {
|
||||||
|
visibleRolesByProject.put(pid, visible);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 6. 我负责的任务计数(owner=me,按项目分组 total + pending)
|
||||||
|
List<String> taskTerminal = objectStatusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
Map<Long, long[]> taskCountMap = new LinkedHashMap<>();
|
||||||
|
for (Map<String, Object> row : projectTaskMapper
|
||||||
|
.selectMyTaskCountGroupByProjectIds(loginUserId, projectIds, taskTerminal)) {
|
||||||
|
taskCountMap.put(asLong(row.get("projectId")),
|
||||||
|
new long[]{asLong(row.get("totalCount")), asLong(row.get("pendingCount"))});
|
||||||
|
}
|
||||||
|
// 7. 组装(仅保留我有可见角色的项目)
|
||||||
|
List<MyProjectParticipatedRespVO> all = projects.stream()
|
||||||
|
.filter(p -> visibleRolesByProject.containsKey(p.getId()))
|
||||||
|
.map(p -> {
|
||||||
|
MyProjectParticipatedRespVO vo = new MyProjectParticipatedRespVO();
|
||||||
|
vo.setId(p.getId());
|
||||||
|
vo.setName(p.getProjectName());
|
||||||
|
vo.setCode(p.getProjectCode());
|
||||||
|
vo.setStatusCode(p.getStatusCode());
|
||||||
|
vo.setStatusName(statusNameMap.get(p.getStatusCode()));
|
||||||
|
vo.setProgress(toProgressInt(p.getProgressRate()));
|
||||||
|
vo.setMyRole(buildMyRole(visibleRolesByProject.get(p.getId()), roleMap));
|
||||||
|
long[] c = taskCountMap.getOrDefault(p.getId(), new long[]{0L, 0L});
|
||||||
|
vo.setMyTaskCount((int) c[0]);
|
||||||
|
vo.setMyPendingTaskCount((int) c[1]);
|
||||||
|
return vo;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
return paginate(all, reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 共用私有方法 ========================
|
||||||
|
|
||||||
|
private Map<String, String> loadStatusNameMap(String objectType) {
|
||||||
|
return objectStatusModelMapper.selectListByObjectTypeEnabled(objectType).stream()
|
||||||
|
.collect(Collectors.toMap(ObjectStatusModelDO::getStatusCode,
|
||||||
|
ObjectStatusModelDO::getStatusName, (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, ObjectRoleRespDTO> loadRoleMap(Set<Long> roleIds) {
|
||||||
|
if (roleIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
List<ObjectRoleRespDTO> roles = objectPermissionApi
|
||||||
|
.getObjectRoleList(roleIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
||||||
|
.getCheckedData();
|
||||||
|
if (roles == null || roles.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
return roles.stream().collect(Collectors.toMap(ObjectRoleRespDTO::getId, Function.identity(), (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 可见角色行:剔除 visible=0 的隐式角色(创建者 / 隐式观察者等业务自动赋予角色);visible=null 或 roleMap 缺失视同可见。 */
|
||||||
|
private List<UserObjectRoleDO> filterVisibleRoleRows(List<UserObjectRoleDO> rows, Map<Long, ObjectRoleRespDTO> roleMap) {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return rows.stream()
|
||||||
|
.filter(r -> {
|
||||||
|
ObjectRoleRespDTO role = roleMap.get(r.getRoleId());
|
||||||
|
return role == null || !Integer.valueOf(0).equals(role.getVisible());
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主角色 + 附加角色名拼接。入参为已过滤的可见角色行(visible=0 隐式角色已在上游剔除)。
|
||||||
|
* 主角色挑选与 ProjectMemberServiceImpl 一致:MANAGER 优先,否则 roleId 最小。
|
||||||
|
*/
|
||||||
|
private String buildMyRole(List<UserObjectRoleDO> rowsVisible, Map<Long, ObjectRoleRespDTO> roleMap) {
|
||||||
|
if (rowsVisible == null || rowsVisible.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
UserObjectRoleDO primary = rowsVisible.stream()
|
||||||
|
.filter(r -> {
|
||||||
|
ObjectRoleRespDTO role = roleMap.get(r.getRoleId());
|
||||||
|
return role != null && Objects.equals(ProjectObjectConstants.MANAGER_ROLE_CODE, role.getCode());
|
||||||
|
})
|
||||||
|
.findFirst()
|
||||||
|
.orElseGet(() -> rowsVisible.stream()
|
||||||
|
.min(Comparator.comparing(UserObjectRoleDO::getRoleId))
|
||||||
|
.orElse(rowsVisible.get(0)));
|
||||||
|
String primaryName = roleName(roleMap, primary.getRoleId());
|
||||||
|
List<String> additional = rowsVisible.stream()
|
||||||
|
.filter(r -> !Objects.equals(r.getId(), primary.getId()))
|
||||||
|
.map(r -> roleName(roleMap, r.getRoleId()))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
StringBuilder sb = new StringBuilder(primaryName == null ? "" : primaryName);
|
||||||
|
for (String n : additional) {
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append(" / ");
|
||||||
|
}
|
||||||
|
sb.append(n);
|
||||||
|
}
|
||||||
|
return sb.length() == 0 ? null : sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String roleName(Map<Long, ObjectRoleRespDTO> roleMap, Long roleId) {
|
||||||
|
ObjectRoleRespDTO role = roleMap.get(roleId);
|
||||||
|
return role == null ? null : role.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer toProgressInt(BigDecimal v) {
|
||||||
|
return v == null ? 0 : v.setScale(0, RoundingMode.HALF_UP).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long asLong(Object v) {
|
||||||
|
return v == null ? 0L : ((Number) v).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> PageResult<T> paginate(List<T> all, PageParam reqVO) {
|
||||||
|
long total = all.size();
|
||||||
|
Integer pageSize = reqVO.getPageSize();
|
||||||
|
if (pageSize == null || pageSize < 0) {
|
||||||
|
return new PageResult<>(all, total);
|
||||||
|
}
|
||||||
|
int pageNo = reqVO.getPageNo() == null || reqVO.getPageNo() < 1 ? 1 : reqVO.getPageNo();
|
||||||
|
int fromIndex = Math.min((pageNo - 1) * pageSize, all.size());
|
||||||
|
int toIndex = Math.min(fromIndex + pageSize, all.size());
|
||||||
|
return new PageResult<>(all.subList(fromIndex, toIndex), total);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<MyProjectOwnedRespVO> getMyOwnedPage(MyProjectPageReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
// 1. 我负责的项目(managerUserId = 登录用户)
|
||||||
|
List<ProjectDO> projects = projectMapper.selectList(new LambdaQueryWrapperX<ProjectDO>()
|
||||||
|
.eq(ProjectDO::getManagerUserId, loginUserId));
|
||||||
|
if (projects.isEmpty()) {
|
||||||
|
return new PageResult<>(Collections.emptyList(), 0L);
|
||||||
|
}
|
||||||
|
// 1.1 仅保留非终态项目(终态项目不在工作台「我的项目」体现),并按创建时间升序(先创建的在前)
|
||||||
|
List<String> projectTerminal = objectStatusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE);
|
||||||
|
projects = projects.stream()
|
||||||
|
.filter(p -> !projectTerminal.contains(p.getStatusCode()))
|
||||||
|
.sorted(PROJECT_CREATE_TIME_ASC)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (projects.isEmpty()) {
|
||||||
|
return new PageResult<>(Collections.emptyList(), 0L);
|
||||||
|
}
|
||||||
|
Set<Long> projectIds = projects.stream()
|
||||||
|
.map(ProjectDO::getId).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
// 2. 终态集
|
||||||
|
List<String> taskTerminal = objectStatusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
List<String> execTerminal = objectStatusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
// 3. 任务数 + 逾期数(一次扫表)
|
||||||
|
Map<Long, long[]> taskMap = new LinkedHashMap<>();
|
||||||
|
for (Map<String, Object> row : projectTaskMapper
|
||||||
|
.selectTaskAndOverdueCountGroupByProjectIds(projectIds, taskTerminal, today)) {
|
||||||
|
taskMap.put(asLong(row.get("projectId")),
|
||||||
|
new long[]{asLong(row.get("taskCount")), asLong(row.get("overdueCount"))});
|
||||||
|
}
|
||||||
|
// 4. 执行数
|
||||||
|
Map<Long, Long> execMap = new LinkedHashMap<>();
|
||||||
|
for (Map<String, Object> row : projectExecutionMapper
|
||||||
|
.selectExecutionCountGroupByProjectIds(projectIds, execTerminal)) {
|
||||||
|
execMap.put(asLong(row.get("projectId")), asLong(row.get("executionCount")));
|
||||||
|
}
|
||||||
|
// 5. 每个负责人(owner)的进行中任务数:projectId -> (ownerId -> count)
|
||||||
|
Map<Long, Map<Long, Long>> activeTaskMap = new LinkedHashMap<>();
|
||||||
|
for (Map<String, Object> row : projectTaskMapper
|
||||||
|
.selectActiveTaskCountGroupByProjectIdAndOwner(projectIds, taskTerminal)) {
|
||||||
|
Long pid = asLong(row.get("projectId"));
|
||||||
|
Long ownerId = asLong(row.get("ownerId"));
|
||||||
|
activeTaskMap.computeIfAbsent(pid, k -> new LinkedHashMap<>())
|
||||||
|
.put(ownerId, asLong(row.get("activeTaskCount")));
|
||||||
|
}
|
||||||
|
// 6. 成员清单(批量一次拿全,内存按项目分组;同 user 多角色去重为一个成员)
|
||||||
|
List<UserObjectRoleDO> memberRows = userObjectRoleMapper
|
||||||
|
.selectActiveListByObjectTypeAndObjectIds(ProjectObjectConstants.OBJECT_TYPE, projectIds);
|
||||||
|
Map<Long, List<Long>> memberUserIdsByProject = new LinkedHashMap<>();
|
||||||
|
for (UserObjectRoleDO m : memberRows) {
|
||||||
|
if (m.getObjectId() == null || m.getUserId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Long> users = memberUserIdsByProject.computeIfAbsent(m.getObjectId(), k -> new ArrayList<>());
|
||||||
|
if (!users.contains(m.getUserId())) {
|
||||||
|
users.add(m.getUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 7. 成员昵称批量回填
|
||||||
|
Set<Long> allUserIds = memberUserIdsByProject.values().stream()
|
||||||
|
.flatMap(List::stream).collect(Collectors.toSet());
|
||||||
|
Map<Long, AdminUserRespDTO> userMap = allUserIds.isEmpty()
|
||||||
|
? Collections.emptyMap() : adminUserApi.getUserMap(allUserIds);
|
||||||
|
// 8. myRole 恒为负责人角色名(一次性解析)
|
||||||
|
String managerRoleName = resolveManagerRoleName();
|
||||||
|
// 9. 组装
|
||||||
|
List<MyProjectOwnedRespVO> all = projects.stream().map(p -> {
|
||||||
|
MyProjectOwnedRespVO vo = new MyProjectOwnedRespVO();
|
||||||
|
vo.setId(p.getId());
|
||||||
|
vo.setName(p.getProjectName());
|
||||||
|
vo.setCode(p.getProjectCode());
|
||||||
|
vo.setProgress(toProgressInt(p.getProgressRate()));
|
||||||
|
vo.setMyRole(managerRoleName);
|
||||||
|
vo.setPlannedEndDate(p.getPlannedEndDate());
|
||||||
|
long[] tc = taskMap.getOrDefault(p.getId(), new long[]{0L, 0L});
|
||||||
|
vo.setTaskCount((int) tc[0]);
|
||||||
|
vo.setOverdueCount((int) tc[1]);
|
||||||
|
vo.setExecutionCount(execMap.getOrDefault(p.getId(), 0L).intValue());
|
||||||
|
List<Long> memberUserIds = memberUserIdsByProject.getOrDefault(p.getId(), Collections.emptyList());
|
||||||
|
vo.setMemberCount(memberUserIds.size());
|
||||||
|
Map<Long, Long> ownerCounts = activeTaskMap.getOrDefault(p.getId(), Collections.emptyMap());
|
||||||
|
List<MyProjectOwnedRespVO.MemberLoadVO> members = memberUserIds.stream().map(uid -> {
|
||||||
|
MyProjectOwnedRespVO.MemberLoadVO mv = new MyProjectOwnedRespVO.MemberLoadVO();
|
||||||
|
mv.setUserId(uid);
|
||||||
|
AdminUserRespDTO user = userMap.get(uid);
|
||||||
|
mv.setUserName(user == null ? null : user.getNickname());
|
||||||
|
mv.setActiveTaskCount(ownerCounts.getOrDefault(uid, 0L).intValue());
|
||||||
|
return mv;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
vo.setMembers(members);
|
||||||
|
return vo;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
return paginate(all, reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 项目负责人角色名(对象域 MANAGER_ROLE_CODE);解析失败返回 null,不阻断列表。 */
|
||||||
|
private String resolveManagerRoleName() {
|
||||||
|
try {
|
||||||
|
ObjectRoleRespDTO role = objectPermissionApi
|
||||||
|
.getObjectRoleByCode(ProjectObjectConstants.MANAGER_ROLE_CODE,
|
||||||
|
ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
||||||
|
.getCheckedData();
|
||||||
|
return role == null ? null : role.getName();
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ public class ProjectMemberServiceImpl implements ProjectMemberService {
|
|||||||
private ProjectExecutionMapper projectExecutionMapper;
|
private ProjectExecutionMapper projectExecutionMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", accessible = true)
|
||||||
public List<ProjectMemberRespVO> getProjectMemberList(Long projectId) {
|
public List<ProjectMemberRespVO> getProjectMemberList(Long projectId) {
|
||||||
ProjectDO project = validateProjectExists(projectId);
|
ProjectDO project = validateProjectExists(projectId);
|
||||||
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId);
|
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.njcn.rdms.framework.common.pojo.PageResult;
|
|||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementAllowedTransitionBatchRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementAllowedTransitionBatchRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementLifecycleRespVO;
|
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO;
|
||||||
@@ -23,94 +22,40 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public interface ProjectRequirementService {
|
public interface ProjectRequirementService {
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建项目需求
|
|
||||||
*/
|
|
||||||
Long createRequirement(ProjectRequirementSaveReqVO createReqVO);
|
Long createRequirement(ProjectRequirementSaveReqVO createReqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新项目需求
|
|
||||||
*/
|
|
||||||
void updateRequirement(ProjectRequirementUpdateReqVO updateReqVO);
|
void updateRequirement(ProjectRequirementUpdateReqVO updateReqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求详情
|
|
||||||
*/
|
|
||||||
ProjectRequirementRespVO getRequirement(Long id, Long projectId);
|
ProjectRequirementRespVO getRequirement(Long id, Long projectId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求分页列表
|
|
||||||
*/
|
|
||||||
PageResult<ProjectRequirementRespVO> getRequirementPage(ProjectRequirementPageReqVO pageReqVO);
|
PageResult<ProjectRequirementRespVO> getRequirementPage(ProjectRequirementPageReqVO pageReqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求树形列表
|
|
||||||
*/
|
|
||||||
PageResult<ProjectRequirementRespVO> getRequirementTree(ProjectRequirementPageReqVO pageReqVO);
|
PageResult<ProjectRequirementRespVO> getRequirementTree(ProjectRequirementPageReqVO pageReqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 变更需求状态
|
|
||||||
*/
|
|
||||||
void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO);
|
void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO);
|
||||||
|
|
||||||
/**
|
void changeRequirementStatusForReview(ProjectRequirementStatusActionReqVO reqVO);
|
||||||
* 删除需求
|
|
||||||
*/
|
|
||||||
void deleteRequirement(Long id, Long projectId);
|
void deleteRequirement(Long id, Long projectId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 拆分需求
|
|
||||||
*/
|
|
||||||
Long splitRequirement(ProjectRequirementSplitReqVO reqVO);
|
Long splitRequirement(ProjectRequirementSplitReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭需求
|
|
||||||
*/
|
|
||||||
void closeRequirement(ProjectRequirementCloseReqVO reqVO);
|
void closeRequirement(ProjectRequirementCloseReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求可执行动作列表
|
|
||||||
*/
|
|
||||||
List<ProjectRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long projectId);
|
List<ProjectRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long projectId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量获取需求可执行动作列表
|
|
||||||
*/
|
|
||||||
List<ProjectRequirementAllowedTransitionBatchRespVO> getAllowedTransitionsBatch(ProjectRequirementBatchReqVO reqVO);
|
List<ProjectRequirementAllowedTransitionBatchRespVO> getAllowedTransitionsBatch(ProjectRequirementBatchReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求生命周期信息
|
|
||||||
*/
|
|
||||||
ProjectRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long projectId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建需求模块
|
|
||||||
*/
|
|
||||||
Long createRequirementModule(ProjectRequirementModuleReqVO reqVO);
|
Long createRequirementModule(ProjectRequirementModuleReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新需求模块
|
|
||||||
*/
|
|
||||||
void updateRequirementModule(ProjectRequirementModuleReqVO reqVO);
|
void updateRequirementModule(ProjectRequirementModuleReqVO reqVO);
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除需求模块
|
|
||||||
*/
|
|
||||||
void deleteRequirementModule(Long moduleId, Long projectId);
|
void deleteRequirementModule(Long moduleId, Long projectId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求模块树
|
|
||||||
*/
|
|
||||||
List<ProjectRequirementModuleRespVO> getRequirementModuleTree(Long projectId);
|
List<ProjectRequirementModuleRespVO> getRequirementModuleTree(Long projectId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求状态字典
|
|
||||||
*/
|
|
||||||
List<ProjectRequirementStatusDictRespVO> getRequirementStatusDict();
|
List<ProjectRequirementStatusDictRespVO> getRequirementStatusDict();
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取需求终态字典
|
|
||||||
*/
|
|
||||||
List<ProjectRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
|
List<ProjectRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
|||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementAllowedTransitionBatchRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementAllowedTransitionBatchRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementLifecycleRespVO;
|
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO;
|
||||||
@@ -42,6 +41,7 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMappe
|
|||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
|
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
|
||||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
|
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
|
||||||
|
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -66,21 +66,23 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
private static final String REQUIREMENT_OBJECT_TYPE = "project_requirement";
|
private static final String REQUIREMENT_OBJECT_TYPE = "project_requirement";
|
||||||
private static final String PROJECT_OBJECT_TYPE = ProjectObjectConstants.OBJECT_TYPE;
|
private static final String PROJECT_OBJECT_TYPE = ProjectObjectConstants.OBJECT_TYPE;
|
||||||
|
|
||||||
private static final String STATUS_PENDING_CONFIRM = "pending_confirm";
|
private static final String STATUS_PENDING_CLAIM = "pending_claim";
|
||||||
private static final String STATUS_PENDING_REVIEW = "pending_review";
|
private static final String STATUS_PENDING_REVIEW = "pending_review";
|
||||||
|
private static final String STATUS_REVIEWED = "reviewed";
|
||||||
|
private static final String STATUS_REVIEW_REJECTED = "review_rejected";
|
||||||
private static final String STATUS_IMPLEMENTING = "implementing";
|
private static final String STATUS_IMPLEMENTING = "implementing";
|
||||||
private static final String STATUS_ACCEPTED = "accepted";
|
private static final String STATUS_ACCEPTED = "accepted";
|
||||||
private static final String STATUS_CLOSED = "closed";
|
private static final String STATUS_CLOSED = "closed";
|
||||||
private static final String STATUS_REJECTED = "rejected";
|
private static final String STATUS_REJECTED = "rejected";
|
||||||
private static final String STATUS_CANCELLED = "cancelled";
|
private static final String STATUS_CANCELLED = "cancelled";
|
||||||
private static final String SOURCE_TYPE_PRODUCT_REQUIREMENT = "product_requirement";
|
private static final String SOURCE_TYPE_PRODUCT_REQUIREMENT = "product_requirement";
|
||||||
|
private static final String SOURCE_TYPE_MANUAL = "manual";
|
||||||
|
private static final String SOURCE_TYPE_WORK_ORDER = "work_order";
|
||||||
|
|
||||||
private static final List<String> TERMINAL_STATUSES = List.of(
|
private static final Set<String> REVIEW_STATUS_CODES = Set.of(
|
||||||
STATUS_CLOSED, STATUS_REJECTED, STATUS_CANCELLED);
|
STATUS_PENDING_REVIEW, STATUS_REVIEWED, STATUS_REVIEW_REJECTED);
|
||||||
private static final List<String> CHILD_ALLOW_CLOSE_STATUSES = List.of(
|
|
||||||
STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED);
|
|
||||||
private static final List<String> ALLOW_DELETE_STATUSES = List.of(
|
private static final List<String> ALLOW_DELETE_STATUSES = List.of(
|
||||||
STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW);
|
STATUS_PENDING_CLAIM, STATUS_PENDING_REVIEW);
|
||||||
private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(
|
private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(
|
||||||
STATUS_REJECTED, STATUS_CANCELLED);
|
STATUS_REJECTED, STATUS_CANCELLED);
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
private static final String PROJECT_QUERY_PERMISSION = "project:project:query";
|
private static final String PROJECT_QUERY_PERMISSION = "project:project:query";
|
||||||
private static final String PROJECT_UPDATE_PERMISSION = ProjectObjectConstants.PERMISSION_UPDATE;
|
private static final String PROJECT_UPDATE_PERMISSION = ProjectObjectConstants.PERMISSION_UPDATE;
|
||||||
private static final String PROJECT_STATUS_PERMISSION = ProjectObjectConstants.PERMISSION_STATUS;
|
private static final String PROJECT_STATUS_PERMISSION = ProjectObjectConstants.PERMISSION_STATUS;
|
||||||
|
private static final String PROJECT_REVIEW_PERMISSION = ProjectObjectConstants.PERMISSION_REVIEW;
|
||||||
private static final String PROJECT_DELETE_PERMISSION = ProjectObjectConstants.PERMISSION_DELETE;
|
private static final String PROJECT_DELETE_PERMISSION = ProjectObjectConstants.PERMISSION_DELETE;
|
||||||
private static final String PROJECT_SPLIT_PERMISSION = ProjectObjectConstants.PERMISSION_SPLIT;
|
private static final String PROJECT_SPLIT_PERMISSION = ProjectObjectConstants.PERMISSION_SPLIT;
|
||||||
|
|
||||||
@@ -98,6 +101,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
private static final String ACTION_CLOSE = "close";
|
private static final String ACTION_CLOSE = "close";
|
||||||
private static final String ACTION_ACCEPT = "accept";
|
private static final String ACTION_ACCEPT = "accept";
|
||||||
private static final String ACTION_CANCEL = "cancel";
|
private static final String ACTION_CANCEL = "cancel";
|
||||||
|
private static final String ACTION_REJECT = "reject";
|
||||||
|
private static final String ACTION_PASS_REVIEW = "pass_review";
|
||||||
|
private static final String ACTION_REJECT_REVIEW = "reject_review";
|
||||||
|
|
||||||
private static final String ACTION_AUTO_DERIVE = "auto_derive";
|
private static final String ACTION_AUTO_DERIVE = "auto_derive";
|
||||||
private static final String ACTION_SYNC_PRODUCT_STATUS = "sync_project_requirement_status";
|
private static final String ACTION_SYNC_PRODUCT_STATUS = "sync_project_requirement_status";
|
||||||
@@ -126,6 +132,8 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||||
@Resource
|
@Resource
|
||||||
private ProjectExecutionMapper projectExecutionMapper;
|
private ProjectExecutionMapper projectExecutionMapper;
|
||||||
|
@Resource
|
||||||
|
private StatusActionTextResolver statusActionTextResolver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -145,7 +153,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
requirement.setTitle(createReqVO.getTitle().trim());
|
requirement.setTitle(createReqVO.getTitle().trim());
|
||||||
requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
|
requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
|
||||||
requirement.setCategory(createReqVO.getCategory());
|
requirement.setCategory(createReqVO.getCategory());
|
||||||
requirement.setSourceType("manual");
|
requirement.setSourceType(SOURCE_TYPE_MANUAL);
|
||||||
requirement.setPriority(createReqVO.getPriority());
|
requirement.setPriority(createReqVO.getPriority());
|
||||||
String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
|
String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
|
||||||
? STATUS_PENDING_REVIEW : STATUS_IMPLEMENTING;
|
? STATUS_PENDING_REVIEW : STATUS_IMPLEMENTING;
|
||||||
@@ -171,7 +179,11 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
public void updateRequirement(ProjectRequirementUpdateReqVO updateReqVO) {
|
public void updateRequirement(ProjectRequirementUpdateReqVO updateReqVO) {
|
||||||
ProjectRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
|
ProjectRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
|
||||||
validateRequirementBelongsToProject(requirement, updateReqVO.getProjectId());
|
validateRequirementBelongsToProject(requirement, updateReqVO.getProjectId());
|
||||||
validateRequirementEditable(requirement);
|
if (!(SOURCE_TYPE_PRODUCT_REQUIREMENT.equals(requirement.getSourceType())
|
||||||
|
&& Objects.equals(requirement.getParentId(), 0L)
|
||||||
|
&& STATUS_IMPLEMENTING.equals(requirement.getStatusCode()))) {
|
||||||
|
validateRequirementEditable(requirement);
|
||||||
|
}
|
||||||
|
|
||||||
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProjectId());
|
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProjectId());
|
||||||
validateModuleBelongsToProject(moduleId, updateReqVO.getProjectId());
|
validateModuleBelongsToProject(moduleId, updateReqVO.getProjectId());
|
||||||
@@ -292,6 +304,19 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
|
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
|
||||||
permission = PROJECT_STATUS_PERMISSION)
|
permission = PROJECT_STATUS_PERMISSION)
|
||||||
public void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO) {
|
public void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO) {
|
||||||
|
doChangeRequirementStatus(reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
|
||||||
|
permission = PROJECT_REVIEW_PERMISSION)
|
||||||
|
public void changeRequirementStatusForReview(ProjectRequirementStatusActionReqVO reqVO) {
|
||||||
|
validateReviewStatusAction(reqVO.getActionCode());
|
||||||
|
doChangeRequirementStatus(reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doChangeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO) {
|
||||||
ProjectRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
ProjectRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
||||||
validateRequirementBelongsToProject(requirement, reqVO.getProjectId());
|
validateRequirementBelongsToProject(requirement, reqVO.getProjectId());
|
||||||
String actionCode = reqVO.getActionCode().trim();
|
String actionCode = reqVO.getActionCode().trim();
|
||||||
@@ -300,6 +325,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
ProjectRequirementDO before = cloneRequirement(requirement);
|
ProjectRequirementDO before = cloneRequirement(requirement);
|
||||||
|
|
||||||
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
||||||
|
validateReviewRejectedActionAllowed(requirement, actionCode);
|
||||||
validateTransitionReason(transition, reason);
|
validateTransitionReason(transition, reason);
|
||||||
String toStatus = transition.getToStatusCode();
|
String toStatus = transition.getToStatusCode();
|
||||||
|
|
||||||
@@ -330,6 +356,13 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
refreshAncestorStatusAndSyncProduct(requirement.getId());
|
refreshAncestorStatusAndSyncProduct(requirement.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateReviewStatusAction(String actionCode) {
|
||||||
|
String normalizedActionCode = actionCode == null ? null : actionCode.trim();
|
||||||
|
if (!ACTION_PASS_REVIEW.equals(normalizedActionCode) && !ACTION_REJECT_REVIEW.equals(normalizedActionCode)) {
|
||||||
|
throw invalidParamException("评审权限只能触发评审通过或评审不通过动作");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId",
|
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId",
|
||||||
@@ -371,7 +404,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
if (!Objects.equals(requirement.getProjectId(), projectId)) {
|
if (!Objects.equals(requirement.getProjectId(), projectId)) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_NOT_BELONG_TO_PROJECT);
|
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_NOT_BELONG_TO_PROJECT);
|
||||||
}
|
}
|
||||||
if (TERMINAL_STATUSES.contains(requirement.getStatusCode())) {
|
if (getTerminalStatusCodes().contains(requirement.getStatusCode())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_TERMINAL);
|
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_TERMINAL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,8 +449,23 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
|
|
||||||
writeBizAuditLog(childRequirement, ACTION_CREATE, null, initialStatus,
|
writeBizAuditLog(childRequirement, ACTION_CREATE, null, initialStatus,
|
||||||
buildRequirementFieldChanges(null, childRequirement), null);
|
buildRequirementFieldChanges(null, childRequirement), null);
|
||||||
writeBizAuditLog(parentRequirement, ACTION_SPLIT, parentRequirement.getStatusCode(),
|
if (STATUS_REVIEWED.equals(parentRequirement.getStatusCode())) {
|
||||||
parentRequirement.getStatusCode(), null, null);
|
ProjectRequirementDO before = cloneRequirement(parentRequirement);
|
||||||
|
String fromStatus = parentRequirement.getStatusCode();
|
||||||
|
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||||
|
parentRequirement.getId(), fromStatus, STATUS_IMPLEMENTING, null);
|
||||||
|
if (updateCount != 1) {
|
||||||
|
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
|
||||||
|
}
|
||||||
|
parentRequirement.setStatusCode(STATUS_IMPLEMENTING);
|
||||||
|
parentRequirement.setLastStatusReason(null);
|
||||||
|
writeRequirementStatusLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null);
|
||||||
|
writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING,
|
||||||
|
buildRequirementFieldChanges(before, parentRequirement), null);
|
||||||
|
} else {
|
||||||
|
writeBizAuditLog(parentRequirement, ACTION_SPLIT, parentRequirement.getStatusCode(),
|
||||||
|
parentRequirement.getStatusCode(), null, null);
|
||||||
|
}
|
||||||
return childRequirement.getId();
|
return childRequirement.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,16 +515,16 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
// 取消动作不满足前置条件时,不再返回给前端展示按钮
|
// 取消动作不满足前置条件时,不再返回给前端展示按钮
|
||||||
.filter(transition -> shouldExposeTransition(requirement, transition))
|
.filter(transition -> shouldExposeTransition(requirement, transition))
|
||||||
.map(transition -> {
|
.map(transition -> {
|
||||||
ProjectRequirementStatusTransitionRespVO vo = new ProjectRequirementStatusTransitionRespVO();
|
ProjectRequirementStatusTransitionRespVO vo = new ProjectRequirementStatusTransitionRespVO();
|
||||||
vo.setActionCode(transition.getActionCode());
|
vo.setActionCode(transition.getActionCode());
|
||||||
vo.setActionName(transition.getActionName());
|
vo.setActionName(transition.getActionName());
|
||||||
vo.setToStatusCode(transition.getToStatusCode());
|
vo.setToStatusCode(transition.getToStatusCode());
|
||||||
ObjectStatusModelDO statusModel = statusModelMapper
|
ObjectStatusModelDO statusModel = statusModelMapper
|
||||||
.selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, transition.getToStatusCode());
|
.selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, transition.getToStatusCode());
|
||||||
vo.setToStatusName(statusModel != null ? statusModel.getStatusName() : transition.getToStatusCode());
|
vo.setToStatusName(statusModel != null ? statusModel.getStatusName() : transition.getToStatusCode());
|
||||||
vo.setNeedReason(transition.getNeedReason());
|
vo.setNeedReason(transition.getNeedReason());
|
||||||
return vo;
|
return vo;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -496,29 +544,6 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId",
|
|
||||||
permission = PROJECT_QUERY_PERMISSION)
|
|
||||||
public ProjectRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long projectId) {
|
|
||||||
ProjectRequirementDO requirement = validateRequirementExists(requirementId);
|
|
||||||
validateRequirementBelongsToProject(requirement, projectId);
|
|
||||||
String currentStatus = requirement.getStatusCode();
|
|
||||||
|
|
||||||
ObjectStatusModelDO statusModel = statusModelMapper
|
|
||||||
.selectByObjectTypeAndStatusCodeEnabled(REQUIREMENT_OBJECT_TYPE, currentStatus);
|
|
||||||
if (statusModel == null) {
|
|
||||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProjectRequirementLifecycleRespVO lifecycle = new ProjectRequirementLifecycleRespVO();
|
|
||||||
lifecycle.setStatusCode(statusModel.getStatusCode());
|
|
||||||
lifecycle.setStatusName(statusModel.getStatusName());
|
|
||||||
lifecycle.setTerminal(statusModel.getTerminalFlag());
|
|
||||||
lifecycle.setAllowEdit(statusModel.getAllowEdit());
|
|
||||||
lifecycle.setLastStatusReason(requirement.getLastStatusReason());
|
|
||||||
lifecycle.setAvailableActions(getAllowedTransitions(requirementId, projectId));
|
|
||||||
return lifecycle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -812,9 +837,11 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
|
void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
|
||||||
List<ProjectRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
List<ProjectRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
||||||
|
Set<String> terminalStatusCodes = getTerminalStatusCodes();
|
||||||
for (ProjectRequirementDO requirement : allChildren) {
|
for (ProjectRequirementDO requirement : allChildren) {
|
||||||
if (!Objects.equals(requirement.getId(), requirementId)
|
if (!Objects.equals(requirement.getId(), requirementId)
|
||||||
&& !CHILD_ALLOW_CLOSE_STATUSES.contains(requirement.getStatusCode())) {
|
&& !STATUS_ACCEPTED.equals(requirement.getStatusCode())
|
||||||
|
&& !terminalStatusCodes.contains(requirement.getStatusCode())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
|
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -838,6 +865,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
* 当前只对取消动作做额外过滤,避免前端展示点了也会报错的按钮。
|
* 当前只对取消动作做额外过滤,避免前端展示点了也会报错的按钮。
|
||||||
*/
|
*/
|
||||||
private boolean shouldExposeTransition(ProjectRequirementDO requirement, ObjectStatusTransitionDO transition) {
|
private boolean shouldExposeTransition(ProjectRequirementDO requirement, ObjectStatusTransitionDO transition) {
|
||||||
|
if (!isReviewRejectedActionAllowed(requirement, transition.getActionCode())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!ACTION_CANCEL.equals(transition.getActionCode())) {
|
if (!ACTION_CANCEL.equals(transition.getActionCode())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -850,12 +880,38 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
return isParentCancelAllowed(requirement.getId());
|
return isParentCancelAllowed(requirement.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评审不通过后的动作由来源决定:手工新增和产品需求子需求只能取消,工单流转只能拒绝。
|
||||||
|
*/
|
||||||
|
private void validateReviewRejectedActionAllowed(ProjectRequirementDO requirement, String actionCode) {
|
||||||
|
if (!isReviewRejectedActionAllowed(requirement, actionCode)) {
|
||||||
|
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode()),
|
||||||
|
statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isReviewRejectedActionAllowed(ProjectRequirementDO requirement, String actionCode) {
|
||||||
|
if (!STATUS_REVIEW_REJECTED.equals(requirement.getStatusCode())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (SOURCE_TYPE_MANUAL.equals(requirement.getSourceType())
|
||||||
|
|| SOURCE_TYPE_PRODUCT_REQUIREMENT.equals(requirement.getSourceType())) {
|
||||||
|
return ACTION_CANCEL.equals(actionCode);
|
||||||
|
}
|
||||||
|
if (SOURCE_TYPE_WORK_ORDER.equals(requirement.getSourceType())) {
|
||||||
|
return ACTION_REJECT.equals(actionCode);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断当前项目需求是否由产品需求流转生成。
|
* 判断当前项目需求是否由产品需求流转生成。
|
||||||
*/
|
*/
|
||||||
private boolean isFromProductRequirement(ProjectRequirementDO requirement) {
|
private boolean isFromProductRequirement(ProjectRequirementDO requirement) {
|
||||||
return Objects.equals(requirement.getSourceType(), SOURCE_TYPE_PRODUCT_REQUIREMENT)
|
return Objects.equals(requirement.getSourceType(), SOURCE_TYPE_PRODUCT_REQUIREMENT)
|
||||||
&& requirement.getProductRequirementId() != null;
|
&& requirement.getProductRequirementId() != null
|
||||||
|
&& Objects.equals(requirement.getParentId(), 0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -996,6 +1052,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
|| rootRequirement.getProductRequirementId() == null) {
|
|| rootRequirement.getProductRequirementId() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (REVIEW_STATUS_CODES.contains(rootRequirement.getStatusCode())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ProductRequirementDO productRequirement = productRequirementMapper.selectById(rootRequirement.getProductRequirementId());
|
ProductRequirementDO productRequirement = productRequirementMapper.selectById(rootRequirement.getProductRequirementId());
|
||||||
if (productRequirement == null || Objects.equals(productRequirement.getStatusCode(), rootRequirement.getStatusCode())) {
|
if (productRequirement == null || Objects.equals(productRequirement.getStatusCode(), rootRequirement.getStatusCode())) {
|
||||||
@@ -1039,6 +1098,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
respVO.setSort(statusModel.getSort());
|
respVO.setSort(statusModel.getSort());
|
||||||
respVO.setInitialFlag(statusModel.getInitialFlag());
|
respVO.setInitialFlag(statusModel.getInitialFlag());
|
||||||
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
||||||
|
respVO.setAllowEdit(statusModel.getAllowEdit());
|
||||||
return respVO;
|
return respVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1058,10 +1118,6 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
ObjectStatusModelDO statusModel = statusModelMap.get(requirement.getStatusCode());
|
ObjectStatusModelDO statusModel = statusModelMap.get(requirement.getStatusCode());
|
||||||
if (statusModel != null) {
|
if (statusModel != null) {
|
||||||
respVO.setStatusName(statusModel.getStatusName());
|
respVO.setStatusName(statusModel.getStatusName());
|
||||||
respVO.setTerminal(statusModel.getTerminalFlag());
|
|
||||||
}
|
|
||||||
if (respVO.getTerminal() == null) {
|
|
||||||
respVO.setTerminal(TERMINAL_STATUSES.contains(requirement.getStatusCode()));
|
|
||||||
}
|
}
|
||||||
return respVO;
|
return respVO;
|
||||||
}
|
}
|
||||||
@@ -1219,14 +1275,24 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateRequirementEditable(ProjectRequirementDO requirement) {
|
void validateRequirementEditable(ProjectRequirementDO requirement) {
|
||||||
if (TERMINAL_STATUSES.contains(requirement.getStatusCode())) {
|
ObjectStatusModelDO statusModel = statusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||||
|
REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode());
|
||||||
|
if (statusModel == null) {
|
||||||
|
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
|
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<String> getTerminalStatusCodes() {
|
||||||
|
return new HashSet<>(statusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE));
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateParentAllowSplit(ProjectRequirementDO parentRequirement) {
|
void validateParentAllowSplit(ProjectRequirementDO parentRequirement) {
|
||||||
if (!STATUS_IMPLEMENTING.equals(parentRequirement.getStatusCode())) {
|
String status = parentRequirement.getStatusCode();
|
||||||
|
if (!STATUS_IMPLEMENTING.equals(status)&& !STATUS_REVIEWED.equals(status)) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_PARENT_NOT_ALLOW_SPLIT);
|
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_PARENT_NOT_ALLOW_SPLIT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1236,7 +1302,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
ObjectStatusTransitionDO transition = statusTransitionMapper
|
ObjectStatusTransitionDO transition = statusTransitionMapper
|
||||||
.selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode);
|
.selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode);
|
||||||
if (transition == null) {
|
if (transition == null) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, fromStatusCode),
|
||||||
|
statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
|
||||||
}
|
}
|
||||||
return transition;
|
return transition;
|
||||||
}
|
}
|
||||||
@@ -1244,7 +1312,7 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
||||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,4 +60,10 @@ public interface ProjectService {
|
|||||||
|
|
||||||
void autoStartProjectIfPending(Long projectId, String triggerAction);
|
void autoStartProjectIfPending(Long projectId, String triggerAction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重算项目进度并落库:AVG(项目下所有根任务 progressRate),沿用 task 维度的 progress_excluded 排除集合。
|
||||||
|
* 由任务侧在"任务进度变化 / 状态变更 / 创建删除 / 父子结构变化"等入口触发。
|
||||||
|
*/
|
||||||
|
void recalcProgress(Long projectId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
|||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
||||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectContextNavRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectContextNavRespVO;
|
||||||
@@ -32,9 +33,11 @@ import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransition
|
|||||||
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectStatusLogMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectStatusLogMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
@@ -43,6 +46,7 @@ import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
|||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
|
||||||
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
|
||||||
|
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectMenuRespDTO;
|
import com.njcn.rdms.module.system.api.permission.dto.ObjectMenuRespDTO;
|
||||||
@@ -58,6 +62,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -108,9 +113,13 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ProjectRequirementModuleMapper projectRequirementModuleMapper;
|
private ProjectRequirementModuleMapper projectRequirementModuleMapper;
|
||||||
@Resource
|
@Resource
|
||||||
|
private ProjectTaskMapper projectTaskMapper;
|
||||||
|
@Resource
|
||||||
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
||||||
@Resource
|
@Resource
|
||||||
private ObjectDataScopeService objectDataScopeService;
|
private ObjectDataScopeService objectDataScopeService;
|
||||||
|
@Resource
|
||||||
|
private StatusActionTextResolver statusActionTextResolver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -366,6 +375,9 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
// 对象域鉴权:是该产品的显式成员即可拿"实现项目"派发下拉数据(memberOnly,不查权限码、不依赖 DB 权限配置)
|
||||||
|
@com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE,
|
||||||
|
objectId = "#productId", memberOnly = true)
|
||||||
public List<ProjectRespVO> getProjectListByProductId(Long productId) {
|
public List<ProjectRespVO> getProjectListByProductId(Long productId) {
|
||||||
validateProductUsable(productId);
|
validateProductUsable(productId);
|
||||||
return projectMapper.selectListByProductId(productId).stream()
|
return projectMapper.selectListByProductId(productId).stream()
|
||||||
@@ -388,7 +400,7 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
// 显式角色为空:走 scope.contains 判定隐式 observer 兜底(设计文档 2.1 节末段)
|
// 显式角色为空:走 scope.contains 判定隐式 observer 兜底(设计文档 2.1 节末段)
|
||||||
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProjectObjectConstants.OBJECT_TYPE);
|
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProjectObjectConstants.OBJECT_TYPE);
|
||||||
if (!scope.contains(id, project.getDirectionCode())) {
|
if (!scope.contains(id, project.getDirectionCode())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED, "查看");
|
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED);
|
||||||
}
|
}
|
||||||
return buildImplicitObserverContext(project);
|
return buildImplicitObserverContext(project);
|
||||||
}
|
}
|
||||||
@@ -575,7 +587,9 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
ProjectDO project = validateProjectExists(reqVO.getId());
|
ProjectDO project = validateProjectExists(reqVO.getId());
|
||||||
String actionCode = reqVO.getActionCode().trim();
|
String actionCode = reqVO.getActionCode().trim();
|
||||||
if (ObjectActivityConstants.PROJECT_ACTION_AUTO_START.equals(actionCode)) {
|
if (ObjectActivityConstants.PROJECT_ACTION_AUTO_START.equals(actionCode)) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode()),
|
||||||
|
statusActionTextResolver.actionName(ProjectObjectConstants.OBJECT_TYPE, actionCode));
|
||||||
}
|
}
|
||||||
changeStatus(project, actionCode, normalizeNullableText(reqVO.getReason()));
|
changeStatus(project, actionCode, normalizeNullableText(reqVO.getReason()));
|
||||||
}
|
}
|
||||||
@@ -608,10 +622,12 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void autoStartProjectIfPending(Long projectId, String triggerAction) {
|
public void autoStartProjectIfPending(Long projectId, String triggerAction) {
|
||||||
// auto_start 只允许由后端业务动作内部触发,前端不应直接透传该动作。
|
// auto_start 只允许由后端业务动作内部触发,前端不应直接透传该动作。
|
||||||
if (!ProjectObjectConstants.AUTO_START_TRIGGERS.contains(triggerAction)) {
|
|
||||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED, triggerAction);
|
|
||||||
}
|
|
||||||
ProjectDO project = validateProjectExists(projectId);
|
ProjectDO project = validateProjectExists(projectId);
|
||||||
|
if (!ProjectObjectConstants.AUTO_START_TRIGGERS.contains(triggerAction)) {
|
||||||
|
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode()),
|
||||||
|
statusActionTextResolver.actionName(ProjectObjectConstants.OBJECT_TYPE, triggerAction));
|
||||||
|
}
|
||||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||||
.selectByObjectTypeAndFromStatusAndAction(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode(),
|
.selectByObjectTypeAndFromStatusAndAction(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode(),
|
||||||
ObjectActivityConstants.PROJECT_ACTION_AUTO_START);
|
ObjectActivityConstants.PROJECT_ACTION_AUTO_START);
|
||||||
@@ -620,7 +636,9 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
ObjectStatusModelDO statusModel = validateEnabledStatusModel(project.getStatusCode());
|
ObjectStatusModelDO statusModel = validateEnabledStatusModel(project.getStatusCode());
|
||||||
if (Boolean.TRUE.equals(statusModel.getInitialFlag())) {
|
if (Boolean.TRUE.equals(statusModel.getInitialFlag())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
ObjectActivityConstants.PROJECT_ACTION_AUTO_START);
|
statusActionTextResolver.statusName(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode()),
|
||||||
|
statusActionTextResolver.actionName(ProjectObjectConstants.OBJECT_TYPE,
|
||||||
|
ObjectActivityConstants.PROJECT_ACTION_AUTO_START));
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
|
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_NOT_ALLOW_EDIT);
|
throw exception(ErrorCodeConstants.PROJECT_STATUS_NOT_ALLOW_EDIT);
|
||||||
@@ -634,6 +652,39 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
changeStatus(project, transition, ObjectActivityConstants.PROJECT_ACTION_AUTO_START, reason);
|
changeStatus(project, transition, ObjectActivityConstants.PROJECT_ACTION_AUTO_START, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void recalcProgress(Long projectId) {
|
||||||
|
if (projectId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProjectDO project = projectMapper.selectById(projectId);
|
||||||
|
if (project == null) {
|
||||||
|
// 项目已被删除(删除项目时其下任务已不可达,无须再触发;此处兜底,避免上游误调)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> excludedStatusCodes = objectStatusModelMapper
|
||||||
|
.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
BigDecimal newProgress = normalizeProgress(projectTaskMapper
|
||||||
|
.selectRootTaskAvgProgressByProjectId(projectId,
|
||||||
|
excludedStatusCodes == null ? Collections.emptyList() : excludedStatusCodes));
|
||||||
|
// 与当前缓存值数值相等则跳过 UPDATE,避免不必要的写与审计字段抖动
|
||||||
|
BigDecimal current = project.getProgressRate();
|
||||||
|
if (current != null && current.compareTo(newProgress) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
projectMapper.updateProgressRateById(projectId, newProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进度归一化:null → 0.00,非 null → scale=2 HALF_UP。与执行/任务层口径一致。
|
||||||
|
*/
|
||||||
|
private BigDecimal normalizeProgress(BigDecimal progress) {
|
||||||
|
if (progress == null) {
|
||||||
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
return progress.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateCreateReqVO(ProjectSaveReqVO createReqVO) {
|
void validateCreateReqVO(ProjectSaveReqVO createReqVO) {
|
||||||
validateProjectCodeUnique(null, createReqVO.getProjectCode());
|
validateProjectCodeUnique(null, createReqVO.getProjectCode());
|
||||||
@@ -734,10 +785,12 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||||
.selectByObjectTypeAndFromStatusAndAction(ProjectObjectConstants.OBJECT_TYPE, fromStatusCode, actionCode);
|
.selectByObjectTypeAndFromStatusAndAction(ProjectObjectConstants.OBJECT_TYPE, fromStatusCode, actionCode);
|
||||||
if (transition == null) {
|
if (transition == null) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_NOT_ALLOWED,
|
||||||
|
statusActionTextResolver.statusName(ProjectObjectConstants.OBJECT_TYPE, fromStatusCode),
|
||||||
|
statusActionTextResolver.actionName(ProjectObjectConstants.OBJECT_TYPE, actionCode));
|
||||||
}
|
}
|
||||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||||
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
throw exception(ErrorCodeConstants.PROJECT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
|
||||||
}
|
}
|
||||||
return transition;
|
return transition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.njcn.rdms.module.project.service.project;
|
package com.njcn.rdms.module.project.service.project;
|
||||||
|
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
|
||||||
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
|
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
|
||||||
|
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.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.ProjectExecutionStatusBoardRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageReqVO;
|
||||||
@@ -12,18 +13,17 @@ import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTask
|
|||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver;
|
|
||||||
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
||||||
|
import com.njcn.rdms.module.project.util.DueRangeSupport;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -42,28 +42,28 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
@Resource
|
@Resource
|
||||||
private ProjectTaskMapper projectTaskMapper;
|
private ProjectTaskMapper projectTaskMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private VisibilityScopeResolver visibilityScopeResolver;
|
|
||||||
@Resource
|
|
||||||
private ProjectTaskService projectTaskService;
|
private ProjectTaskService projectTaskService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||||
public ProjectExecutionStatusBoardRespVO getExecutionStatusBoard(Long projectId, ProjectExecutionStatusBoardReqVO reqVO) {
|
public ProjectExecutionStatusBoardRespVO getExecutionStatusBoard(Long projectId, ProjectExecutionStatusBoardReqVO reqVO) {
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
|
||||||
VisibilityScope scope = visibilityScopeResolver.resolveForProject(projectId, userId);
|
|
||||||
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
||||||
return buildExecutionStatusBoard(projectId, scope, reqVO, statusModels);
|
return buildExecutionStatusBoard(projectId, reqVO, statusModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
public ProjectTaskStatusBoardRespVO getTaskStatusBoard(Long projectId, Long executionId, ProjectTaskStatusBoardReqVO reqVO) {
|
public ProjectTaskStatusBoardRespVO getTaskStatusBoard(Long projectId, Long executionId, ProjectTaskStatusBoardReqVO reqVO) {
|
||||||
VisibilityScope scope = resolveTaskScope(projectId, executionId);
|
|
||||||
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
return buildTaskStatusBoard(projectId, executionId, scope, reqVO, statusModels);
|
return buildTaskStatusBoard(projectId, executionId, reqVO, statusModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
public ProjectTaskBoardPageRespVO getTaskBoardPage(Long projectId, Long executionId, ProjectTaskBoardPageReqVO reqVO) {
|
public ProjectTaskBoardPageRespVO getTaskBoardPage(Long projectId, Long executionId, ProjectTaskBoardPageReqVO reqVO) {
|
||||||
VisibilityScope scope = resolveTaskScope(projectId, executionId);
|
|
||||||
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
|
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
|
||||||
.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
|
|
||||||
ProjectTaskBoardPageRespVO respVO = new ProjectTaskBoardPageRespVO();
|
ProjectTaskBoardPageRespVO respVO = new ProjectTaskBoardPageRespVO();
|
||||||
List<ProjectTaskBoardPageRespVO.ColumnItemVO> items = targetStatusModels.stream()
|
List<ProjectTaskBoardPageRespVO.ColumnItemVO> items = targetStatusModels.stream()
|
||||||
.map(statusModel -> buildBoardColumn(projectId, executionId, scope, reqVO, statusModel))
|
.map(statusModel -> buildBoardColumn(projectId, executionId, reqVO, statusModel))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
respVO.setItems(items);
|
respVO.setItems(items);
|
||||||
return respVO;
|
return respVO;
|
||||||
@@ -98,11 +98,10 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ProjectTaskBoardPageRespVO.ColumnItemVO buildBoardColumn(Long projectId, Long executionId,
|
private ProjectTaskBoardPageRespVO.ColumnItemVO buildBoardColumn(Long projectId, Long executionId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectTaskBoardPageReqVO reqVO,
|
ProjectTaskBoardPageReqVO reqVO,
|
||||||
ObjectStatusModelDO statusModel) {
|
ObjectStatusModelDO statusModel) {
|
||||||
ProjectTaskPageReqVO innerReq = toInnerPageReq(reqVO, statusModel.getStatusCode());
|
ProjectTaskPageReqVO innerReq = toInnerPageReq(reqVO, statusModel.getStatusCode());
|
||||||
PageResult<ProjectTaskDO> doPage = projectTaskMapper.selectPageByExecutionId(projectId, executionId, scope, innerReq);
|
PageResult<ProjectTaskDO> doPage = projectTaskMapper.selectPageByExecutionId(projectId, executionId, innerReq);
|
||||||
PageResult<ProjectTaskRespVO> voPage = projectTaskService.assembleTaskRespVOPage(projectId, executionId, doPage);
|
PageResult<ProjectTaskRespVO> voPage = projectTaskService.assembleTaskRespVOPage(projectId, executionId, doPage);
|
||||||
|
|
||||||
ProjectTaskBoardPageRespVO.ColumnItemVO item = new ProjectTaskBoardPageRespVO.ColumnItemVO();
|
ProjectTaskBoardPageRespVO.ColumnItemVO item = new ProjectTaskBoardPageRespVO.ColumnItemVO();
|
||||||
@@ -124,6 +123,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
innerReq.setPageSize(reqVO.getPageSize());
|
innerReq.setPageSize(reqVO.getPageSize());
|
||||||
innerReq.setKeyword(reqVO.getKeyword());
|
innerReq.setKeyword(reqVO.getKeyword());
|
||||||
innerReq.setParentTaskId(reqVO.getParentTaskId());
|
innerReq.setParentTaskId(reqVO.getParentTaskId());
|
||||||
|
innerReq.setInvolveUserId(reqVO.getInvolveUserId());
|
||||||
innerReq.setOwnerId(reqVO.getOwnerId());
|
innerReq.setOwnerId(reqVO.getOwnerId());
|
||||||
innerReq.setPriority(reqVO.getPriority());
|
innerReq.setPriority(reqVO.getPriority());
|
||||||
innerReq.setUpdateTime(reqVO.getUpdateTime());
|
innerReq.setUpdateTime(reqVO.getUpdateTime());
|
||||||
@@ -131,34 +131,24 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
return innerReq;
|
return innerReq;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算任务可见性 scope,与 ProjectTaskServiceImpl#computeTaskScope 同款:
|
|
||||||
* 项目经理 → seesAll;执行负责人 = 当前用户 → seesAll;否则按 resolveForExecution 求并集。
|
|
||||||
*/
|
|
||||||
private VisibilityScope resolveTaskScope(Long projectId, Long executionId) {
|
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
|
||||||
VisibilityScope scope = visibilityScopeResolver.resolveForExecution(projectId, executionId, userId);
|
|
||||||
if (scope.seesAll()) {
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
ProjectExecutionDO exec = projectExecutionMapper.selectByProjectIdAndId(projectId, executionId);
|
|
||||||
if (exec != null && Objects.equals(exec.getOwnerId(), userId)) {
|
|
||||||
return VisibilityScope.all();
|
|
||||||
}
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProjectExecutionStatusBoardRespVO buildExecutionStatusBoard(Long projectId,
|
private ProjectExecutionStatusBoardRespVO buildExecutionStatusBoard(Long projectId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectExecutionStatusBoardReqVO reqVO,
|
ProjectExecutionStatusBoardReqVO reqVO,
|
||||||
List<ObjectStatusModelDO> statusModels) {
|
List<ObjectStatusModelDO> statusModels) {
|
||||||
|
// dueRange 截止时间过滤所需的日期边界与执行终态码(终态排除口径对齐任务 summary)。
|
||||||
|
LocalDate today = DueRangeSupport.today();
|
||||||
|
LocalDate weekStart = DueRangeSupport.weekStart(today);
|
||||||
|
LocalDate weekEnd = DueRangeSupport.weekEnd(today);
|
||||||
|
List<String> terminalStatusCodes = objectStatusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
||||||
|
|
||||||
ProjectExecutionStatusBoardRespVO respVO = new ProjectExecutionStatusBoardRespVO();
|
ProjectExecutionStatusBoardRespVO respVO = new ProjectExecutionStatusBoardRespVO();
|
||||||
List<ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO> items = statusModels.stream().map(statusModel -> {
|
List<ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO> items = statusModels.stream().map(statusModel -> {
|
||||||
ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO item =
|
ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO item =
|
||||||
new ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO();
|
new ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO();
|
||||||
item.setStatusCode(statusModel.getStatusCode());
|
item.setStatusCode(statusModel.getStatusCode());
|
||||||
item.setStatusName(statusModel.getStatusName());
|
item.setStatusName(statusModel.getStatusName());
|
||||||
item.setCount(projectExecutionMapper.countByProjectIdAndStatusCode(projectId, scope, reqVO, statusModel.getStatusCode()).longValue());
|
item.setCount(projectExecutionMapper.countByProjectIdAndStatusCode(projectId, reqVO,
|
||||||
|
statusModel.getStatusCode(), terminalStatusCodes, today, weekStart, weekEnd).longValue());
|
||||||
item.setSort(statusModel.getSort());
|
item.setSort(statusModel.getSort());
|
||||||
item.setTerminal(statusModel.getTerminalFlag());
|
item.setTerminal(statusModel.getTerminalFlag());
|
||||||
return item;
|
return item;
|
||||||
@@ -169,7 +159,6 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ProjectTaskStatusBoardRespVO buildTaskStatusBoard(Long projectId, Long executionId,
|
private ProjectTaskStatusBoardRespVO buildTaskStatusBoard(Long projectId, Long executionId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectTaskStatusBoardReqVO reqVO,
|
ProjectTaskStatusBoardReqVO reqVO,
|
||||||
List<ObjectStatusModelDO> statusModels) {
|
List<ObjectStatusModelDO> statusModels) {
|
||||||
ProjectTaskStatusBoardRespVO respVO = new ProjectTaskStatusBoardRespVO();
|
ProjectTaskStatusBoardRespVO respVO = new ProjectTaskStatusBoardRespVO();
|
||||||
@@ -177,7 +166,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO item = new ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO();
|
ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO item = new ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO();
|
||||||
item.setStatusCode(statusModel.getStatusCode());
|
item.setStatusCode(statusModel.getStatusCode());
|
||||||
item.setStatusName(statusModel.getStatusName());
|
item.setStatusName(statusModel.getStatusName());
|
||||||
item.setCount(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, scope, reqVO,
|
item.setCount(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, reqVO,
|
||||||
statusModel.getStatusCode()).longValue());
|
statusModel.getStatusCode()).longValue());
|
||||||
item.setSort(statusModel.getSort());
|
item.setSort(statusModel.getSort());
|
||||||
item.setTerminal(statusModel.getTerminalFlag());
|
item.setTerminal(statusModel.getTerminalFlag());
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ public class ProjectExecutionAssigneeServiceImpl implements ProjectExecutionAssi
|
|||||||
private AdminUserApi adminUserApi;
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||||
public List<ExecutionAssigneeRespVO> getExecutionAssigneeList(Long projectId, Long executionId) {
|
public List<ExecutionAssigneeRespVO> getExecutionAssigneeList(Long projectId, Long executionId) {
|
||||||
validateProjectExists(projectId);
|
validateProjectExists(projectId);
|
||||||
validateExecutionExists(projectId, executionId);
|
validateExecutionExists(projectId, executionId);
|
||||||
@@ -150,6 +152,8 @@ public class ProjectExecutionAssigneeServiceImpl implements ProjectExecutionAssi
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||||
public PageResult<ExecutionAssigneeLogRespVO> getExecutionAssigneeLogPage(Long projectId, Long executionId,
|
public PageResult<ExecutionAssigneeLogRespVO> getExecutionAssigneeLogPage(Long projectId, Long executionId,
|
||||||
ExecutionAssigneeLogPageReqVO reqVO) {
|
ExecutionAssigneeLogPageReqVO reqVO) {
|
||||||
validateProjectExists(projectId);
|
validateProjectExists(projectId);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user