Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f03dc27cc | |||
| d669d53a80 | |||
| df13a90107 | |||
| 8a36b49128 | |||
| c9549bed46 | |||
| 5caf3bbdc9 | |||
|
|
9e4f8becc8 | ||
| 58eed8234a | |||
|
|
b4e9685344 | ||
|
|
2ad9a4e206 | ||
| fd637ae604 | |||
| 1bee5eb05b | |||
|
|
19637d74a4 | ||
|
|
d069948d2a | ||
|
|
3199c876c3 | ||
|
|
b6d31ab156 | ||
| 1ef86fc1cb | |||
| 75886d7af5 | |||
| 50b84a57bb | |||
|
|
bd05f6d593 | ||
| 470096aa9a | |||
| 4ad2ddeabe | |||
|
|
9ee49b1863 | ||
| be7e0d6162 | |||
| 8f6b762bf3 | |||
| 3946c0a0aa | |||
| e1db030c37 | |||
| 544b56a5d9 | |||
| 7b4edd6b59 | |||
| 43d8be724e | |||
| 220dec9b6c | |||
| 4f6b209c3d | |||
| 604bf61981 | |||
| 7575784c01 | |||
| 7399f8c3da | |||
| 6f33ab9c05 |
115
.claude/settings.local.json
Normal file
115
.claude/settings.local.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git *)",
|
||||
"Bash(cmd *)",
|
||||
"PowerShell('--rdms-project--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project' | Sort-Object; '--rdms-system--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-system' | Sort-Object; '--rdms-framework--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-framework' | Sort-Object; '--rdms-gateway--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-gateway' | Sort-Object)",
|
||||
"PowerShell('--docs--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\docs' -Recurse -Depth 2 | Sort-Object)",
|
||||
"PowerShell('--project-boot src--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project' -Recurse -Depth 2 | Sort-Object)",
|
||||
"PowerShell('--project-api src--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-api\\\\src\\\\main\\\\java' -Recurse -Depth 5 | Sort-Object)",
|
||||
"PowerShell('--system-boot src--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-system\\\\rdms-system-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\system' -Recurse -Depth 2 | Sort-Object)",
|
||||
"PowerShell('--project-boot service/project--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\service\\\\project' -Recurse -Depth 3 | Sort-Object)",
|
||||
"PowerShell('--project-boot controller--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\controller' -Recurse -Depth 4 | Sort-Object)",
|
||||
"PowerShell('--project-api full--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-api\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project' -Recurse | Sort-Object)",
|
||||
"PowerShell('--project-boot dal--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\dal' -Recurse | Sort-Object)",
|
||||
"PowerShell('--project-api enums--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-api\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\enums' -Recurse | Sort-Object; '--system-api full--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-system\\\\rdms-system-api\\\\src\\\\main\\\\java' -Recurse -Depth 6 | Sort-Object)",
|
||||
"PowerShell('--project-boot framework--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\framework' -Recurse | Sort-Object)",
|
||||
"PowerShell('--sql resources--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\resources' -Recurse | Sort-Object; '--system sql--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-system\\\\rdms-system-boot\\\\src\\\\main\\\\resources\\\\sql' -Recurse | Sort-Object)",
|
||||
"PowerShell('--project-boot resources sql--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\resources\\\\sql' -Recurse | Sort-Object)",
|
||||
"WebSearch",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -Dtest=TaskAssigneeServiceImplTest test 2>&1 | Select-Object -Last 60)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am -Dtest=TaskAssigneeServiceImplTest test 2>&1 | Select-Object -Last 80)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am -Dtest=TaskAssigneeServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test 2>&1 | Select-Object -Last 80)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am \"-Dtest=TaskAssigneeServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 80)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am \"-Dtest=TaskAssigneeServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 50)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am test 2>&1 | Select-Object -Last 80)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot \"-Dtest=TaskAssigneeServiceImplTest,ProjectTaskServiceImplTest,ProjectTaskStatusViewServiceTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 30)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am \"-Dtest=TaskAssigneeServiceImplTest,ProjectTaskServiceImplTest,ProjectTaskStatusViewServiceTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 30)",
|
||||
"Bash(grep -rn \"INSERT INTO \\\\`system_menu\\\\`\\\\|INSERT INTO system_menu\" --include=\"*.sql\" rdms-project rdms-system)",
|
||||
"Bash(findstr /i \"plugin\")",
|
||||
"Bash(findstr *)",
|
||||
"Bash(awk END{print NR} *)",
|
||||
"PowerShell(Move-Item *)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dtest=VisibilityScopeResolverImplTest' -q 2>&1 | Select-Object -Last 80)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' -q 2>&1 | Select-Object -Last 60)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' -q | Select-Object -Last 60)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|ERROR|FAIL' | Select-Object -Last 40)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|ERROR|FAILED|FAIL' | Select-Object -Last 100)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am test-compile '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'ERROR|BUILD|FAIL' | Select-Object -Last 40)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot test '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|FAILED|ERROR' | Select-Object -Last 80)",
|
||||
"Skill(code-review:code-review)",
|
||||
"Bash(Test-Path *)",
|
||||
"Skill(superpowers:systematic-debugging)",
|
||||
"Bash(mv \"项目框架设计与后续改造约束说明.md\" architecture/)",
|
||||
"Bash(mv \"新增微服务模块必需项清单.md\" architecture/)",
|
||||
"Bash(mv \"后端提交规范与示例.md\" architecture/)",
|
||||
"Bash(mv \"单体启动模式可行性评估.md\" architecture/)",
|
||||
"Bash(mv \"对象状态能力落地规范.md\" architecture/)",
|
||||
"Bash(mv \"rdms-gateway.md\" \"modules/网关说明.md\")",
|
||||
"Bash(mv \"rdms-spring-boot-starter-biz-ip.md\" \"modules/业务IP启动器.md\")",
|
||||
"Bash(mv \"rdms-spring-boot-starter-env.md\" \"modules/环境配置启动器.md\")",
|
||||
"Bash(mv \"rdms-spring-boot-starter-excel.md\" \"modules/Excel启动器.md\")",
|
||||
"Bash(mv \"rdms-spring-boot-starter-mq.md\" \"modules/消息队列启动器.md\")",
|
||||
"Bash(mv \"文件存储_内网MinIO接入说明.md\" domains/system/)",
|
||||
"Bash(mv \"登录验证码前端接入说明.md\" domains/system/)",
|
||||
"Bash(mv \"product/02-产品管理_业务设计.md\" domains/product/)",
|
||||
"Bash(mv \"product/03-工单到任务全链路与工作流方案.md\" domains/product/)",
|
||||
"Bash(mv \"product/05-产品管理_前端联调最小闭环清单.md\" domains/product/)",
|
||||
"Bash(mv \"product/06-产品设置_补丁版说明.md\" domains/product/)",
|
||||
"Bash(mv \"product/07-产品设置_前端联调API文档.md\" domains/product/)",
|
||||
"Bash(mv \"项目/03-项目管理_业务设计.md\" domains/project/)",
|
||||
"Bash(mv \"项目/2026-05-08-execution-member-change-history-design.md\" \"domains/project/2026-05-08-执行成员变更历史设计.md\")",
|
||||
"Bash(mv \"项目/2026-05-09-task-fix-plan.md\" \"domains/project/2026-05-09-任务修复计划.md\")",
|
||||
"Bash(mv \"项目/2026-05-09-task-logic-review.md\" \"domains/project/2026-05-09-任务逻辑评审.md\")",
|
||||
"Bash(mv \"项目/2026-05-09-task-worklog-design.md\" \"domains/project/2026-05-09-任务工时设计.md\")",
|
||||
"Bash(mv \"任务工时与进度模型_业内标杆调研.md\" research/)",
|
||||
"Bash(mv \"任务负责人模型_业内标杆调研.md\" research/)",
|
||||
"Bash(mv \"技术负债台账.html\" debt/)",
|
||||
"Bash(mv \"product/product-overview-mockup.html\" \"temp/产品概览-mockup.html\")",
|
||||
"Bash(mv \"项目/2026-05-12-按钮可见度演示.html\" temp/)",
|
||||
"Bash(mv \"product/rdms_object_status_transition.sql\" sql/)",
|
||||
"Bash(mv \"product/system_menu.sql\" sql/)",
|
||||
"Bash(mv \"项目/object.sql\" sql/)",
|
||||
"Bash(mv \"项目/rdms_biz_audit_log.sql\" sql/)",
|
||||
"Bash(mv \"项目/sql/\"*.sql sql/)",
|
||||
"Bash(mv \"项目/项目管理待确认项清单_V1.0.md\" domains/project/)",
|
||||
"Bash(rmdir 项目)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot compile -am -DskipTests 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\" -pl rdms-project/rdms-project-boot compile -am -DskipTests -f \"C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\pom.xml\" 2>&1 | Select-Object -Last 30)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot test-compile -am 2>&1 | Select-Object -Last 40)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot test -am \"-Dtest=ProjectTaskServiceImplTest,ProjectExecutionServiceImplTest\" 2>&1 | Select-Object -Last 80)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot test -am \"-Dtest=ProjectTaskServiceImplTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" 2>&1 | Select-Object -Last 80)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot test -am \"-Dtest=ProjectTaskServiceImplTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" 2>&1 | Select-Object -First 200)",
|
||||
"PowerShell(Get-ChildItem \"C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\.claude\\\\worktrees\\\\agent-a0979555dc2fe9384\\\\rdms-project\\\\rdms-project-boot\\\\target\\\\surefire-reports\" | Where-Object { $_.Name -match \"Test\" } | ForEach-Object { $_.Name })",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot compile -am -DskipTests 2>&1 | Select-Object -Last 30)",
|
||||
"Bash(/c/software/apache-maven-3.8.9/bin/mvn.cmd -pl rdms-project/rdms-project-boot test -am -Dtest=ProjectTaskServiceImplTest#changeTaskStatus_shouldUseTransitionAndWriteLogs -q)",
|
||||
"Bash(/c/software/apache-maven-3.8.9/bin/mvn.cmd -pl rdms-project/rdms-project-boot test -Dtest=ProjectTaskServiceImplTest#changeTaskStatus_shouldUseTransitionAndWriteLogs -Dsurefire.failIfNoSpecifiedTests=false -q)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot test \"-Dtest=ProjectTaskServiceImplTest#changeTaskStatus_shouldUseTransitionAndWriteLogs\" \"-Dsurefire.failIfNoSpecifiedTests=false\" -q 2>&1 | Select-Object -Last 60)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot test-compile -am -q 2>&1 | Select-Object -Last 40)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot test-compile -am -q 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\" -pl rdms-project/rdms-project-boot test-compile -am 2>&1 | Select-String \"BUILD SUCCESS|BUILD FAILURE|ERROR\" | Select-Object -Last 5)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot test -am \"-Dtest=ProjectTaskServiceImplTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" 2>&1 | Select-String \"Tests run|BUILD SUCCESS|BUILD FAILURE|FAILED|<<<\" | Select-Object -Last 30)",
|
||||
"PowerShell([System.IO.Directory]::GetCurrentDirectory\\(\\))",
|
||||
"Bash(Get-ChildItem -Directory -Name)",
|
||||
"Bash(grep \"\\\\.java$\")",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests 2>&1 | Select-Object -Last 30)",
|
||||
"PowerShell($env:JAVA_HOME = \"C:\\\\Program Files\\\\Java\\\\jdk-17\"; & \"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am clean compile -DskipTests 2>&1 | Select-Object -Last 35)",
|
||||
"Bash(C:/Program Files/Java/jdk-17/bin/java.exe *)",
|
||||
"Bash(export JAVA_HOME=\"C:/Program Files/Java/jdk-17\")",
|
||||
"Bash(cd \"C:/code/gitea/rdms/cn-rdms\")",
|
||||
"Bash(\"C:/software/apache-maven-3.8.9/bin/mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests | Select-Object -Last 15)",
|
||||
"PowerShell(java *)",
|
||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests -q 2>&1 | Select-Object -Last 30)",
|
||||
"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(\"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(grep -E \"\\\\.\\(sql|java|md\\)$\")",
|
||||
"Bash(xargs grep -l \"INSERT INTO.*system_menu\")",
|
||||
"Bash(Get-ChildItem *)",
|
||||
"Bash(Select-Object FullName)"
|
||||
]
|
||||
}
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -76,3 +76,6 @@ sessionStore
|
||||
|
||||
# local docs
|
||||
/docs/
|
||||
|
||||
# Claude Code 本地工作区
|
||||
.claude/
|
||||
|
||||
238
AGENTS.md
238
AGENTS.md
@@ -1,239 +1,5 @@
|
||||
# AGENTS.md
|
||||
|
||||
## 适用范围
|
||||
本仓库的 Agent 工作指引以 [`CLAUDE.md`](./CLAUDE.md) 为准。
|
||||
|
||||
本说明适用于以 `C:\code\gitea\rdms\cn-rdms` 为根目录的整个仓库。
|
||||
|
||||
描述仓库现状时,以当前代码、当前配置、当前文档中可直接验证的事实为准;除非用户明确要求,不引入历史实现、过渡方案或已废弃模型来解释当前状态。
|
||||
|
||||
默认回答保持精简,优先给结论、改动点和必要风险,不做过多展开;如果存在你关心但未展开的细节,由你继续追问后再补充。
|
||||
|
||||
## 交互原则
|
||||
|
||||
- 默认先给执行方案,说明目标、涉及模块、预计改动点和验证方式。
|
||||
- 在用户评审并明确同意前,不直接开始实际修改、编译、测试、打包或其他执行动作。
|
||||
- 是否执行由用户决定;如果用户只要求分析、审阅或出方案,就停留在分析和方案层。
|
||||
|
||||
## 项目概览
|
||||
|
||||
这是一个面向 RDMS 服务的多模块 Maven 单仓库项目。
|
||||
|
||||
- Java 版本:17
|
||||
- 构建工具:Maven
|
||||
- 根模块打包类型:`pom`
|
||||
- Spring Boot 版本:`3.5.9`
|
||||
|
||||
## 本地环境约定
|
||||
|
||||
- 本机 Maven 安装路径:`C:\software\apache-maven-3.8.9`
|
||||
- 如需执行 Maven 命令,优先使用完整路径:`C:\software\apache-maven-3.8.9\bin\mvn.cmd`
|
||||
- 不要假设 `mvn` 已加入 PATH
|
||||
- 只有在用户已明确同意执行编译、测试、打包等 Maven 命令时,才使用上述路径执行
|
||||
|
||||
顶层模块:
|
||||
|
||||
1. `rdms-system`
|
||||
2. `rdms-project`
|
||||
3. `rdms-framework`
|
||||
4. `rdms-gateway`
|
||||
|
||||
当前系统域能力主要集中在 `rdms-system`,RDMS 核心交付域能力主要集中在 `rdms-project`,但这只是现阶段结构,不应被理解为长期只保留这两个业务模块。
|
||||
|
||||
后续如果新增独立业务服务,例如项目/产品管理模块、工作流模块,应继续沿用当前仓库的模块拆分方式,而不是把所有后续业务长期堆进 `rdms-system`。
|
||||
|
||||
## 模块说明
|
||||
|
||||
### `rdms-system`
|
||||
|
||||
当前已存在的系统业务聚合模块。
|
||||
|
||||
- `rdms-system/rdms-system-boot`
|
||||
- 主应用模块
|
||||
- 启动入口:`rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/SystemServerApplication.java`
|
||||
- 主包路径:`com.njcn.rdms.module.system`
|
||||
- 常见子包:`api`、`controller`、`convert`、`dal`、`framework`、`job`、`service`、`util`、`websocket`
|
||||
- `rdms-system/rdms-system-api`
|
||||
- 供其他服务依赖的共享 API 模块
|
||||
- 包含对外 API 契约与枚举定义
|
||||
|
||||
说明:
|
||||
|
||||
- 当前权限、用户、组织、岗位、菜单、角色等系统核心能力主要落在这里。
|
||||
- 如果后续只是给系统域补充新的系统子能力,可以继续在 `rdms-system` 内按现有结构扩展。
|
||||
- 如果后续形成独立业务域,例如 `rdms-project`、`rdms-workflow`,应优先建设为新的独立业务模块,而不是默认继续塞进 `rdms-system`。
|
||||
|
||||
### `rdms-project`
|
||||
|
||||
当前已存在的 RDMS 核心交付业务聚合模块。
|
||||
|
||||
- `rdms-project/rdms-project-boot`
|
||||
- 主应用模块
|
||||
- 启动入口:`rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java`
|
||||
- 主包路径:`com.njcn.rdms.module.project`
|
||||
- 常见子包:`api`、`controller`、`convert`、`dal`、`framework`、`service`
|
||||
- `rdms-project/rdms-project-api`
|
||||
- 供其他服务依赖的共享 API 模块
|
||||
- 包含对外 API 契约与枚举定义
|
||||
|
||||
说明:
|
||||
|
||||
- 当前项目集、项目、产品、需求、任务、工单、执行等 RDMS 核心业务能力应优先落在这里。
|
||||
- 需要复用用户、组织、岗位、权限等系统能力时,应通过 `rdms-system-api` 调用,不要反向依赖 `rdms-system-boot`。
|
||||
|
||||
### `rdms-framework`
|
||||
|
||||
共享框架与内部 starter 模块。
|
||||
|
||||
- `rdms-framework/rdms-common`
|
||||
- 核心通用工具与公共抽象
|
||||
- 其他 `rdms-spring-boot-starter-*` 模块
|
||||
- 内部 starter,覆盖 `env`、`web`、`rpc`、`security`、`mybatis`、`redis`、`mq`、`websocket`、`excel`、`protection`、`test`、`biz-ip`
|
||||
|
||||
### `rdms-gateway`
|
||||
|
||||
Spring Cloud Gateway 网关服务。
|
||||
|
||||
- 启动入口:`rdms-gateway/src/main/java/com/njcn/rdms/gateway/GatewayServerApplication.java`
|
||||
- 主包路径:`com.njcn.rdms.gateway`
|
||||
- 常见子包:`filter`、`handler`、`jackson`、`route`、`util`
|
||||
|
||||
## 模块演进约束
|
||||
|
||||
后续新增业务能力时,先区分下面两种情况,不要混用:
|
||||
|
||||
1. 新增独立微服务模块,例如 `rdms-project`、`rdms-workflow`
|
||||
2. 只是在现有 `rdms-system` 中新增一个业务子域
|
||||
|
||||
### 新增独立微服务模块
|
||||
|
||||
如果后续能力已经具备独立服务边界,应优先按下面结构建设:
|
||||
|
||||
```text
|
||||
rdms-xxx
|
||||
├─ rdms-xxx-api
|
||||
└─ rdms-xxx-boot
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- 根 `pom.xml` 增加新的聚合模块
|
||||
- `api` 模块承载对外 RPC/Feign 接口、DTO、错误码、枚举、常量
|
||||
- `boot` 模块承载启动类、controller、service、dal、convert、api 实现、模块级 framework 配置和资源文件
|
||||
- 包路径、`spring.application.name`、`ApiConstants`、`RpcConstants`、`rdms.info.base-package` 必须保持一致
|
||||
- 新服务不是简单复制 `rdms-system` 的名字,而是复用它的工程骨架和分层习惯
|
||||
|
||||
### 在 `rdms-system` 中新增业务子域
|
||||
|
||||
如果只是给系统服务补一个当前阶段仍适合放在 `rdms-system` 内的子域,则继续沿用现有结构:
|
||||
|
||||
- `controller/admin/...` 或 `controller/app/...`
|
||||
- `service/...`
|
||||
- `dal/dataobject/...`
|
||||
- `dal/mysql/...`
|
||||
- `convert/...`
|
||||
- 需要跨模块暴露时,在 `rdms-system-api` 中补 API、DTO、错误码、枚举
|
||||
|
||||
约束:
|
||||
|
||||
- 不要为了新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层语言
|
||||
- 不要让外部模块直接依赖 `rdms-system-boot` 的 service 或 mapper
|
||||
- 如果某项能力未来明显会演进成独立微服务,文档和实现上都要避免把它写死成只能存在于 `rdms-system`
|
||||
|
||||
## 代码目录
|
||||
|
||||
- Java 源码:`*/src/main/java`
|
||||
- 资源文件:`*/src/main/resources`
|
||||
- 测试代码:`*/src/test/java`
|
||||
- 本地辅助脚本:`scripts/`
|
||||
|
||||
## 分层职责约束
|
||||
|
||||
### `rdms-framework`
|
||||
|
||||
- `rdms-framework` 承担基础能力,不承载具体业务语义。
|
||||
- 除非出现框架级缺陷,或该能力明确属于全局可复用基础设施,否则不要把业务判断硬塞进 framework。
|
||||
|
||||
### `rdms-gateway`
|
||||
|
||||
- `rdms-gateway` 只负责统一入口、令牌校验、登录用户透传、路由和网关层横切逻辑。
|
||||
- 不要在 gateway 层承载组织、成员、负责人、项目、产品、工作流状态流转或数据可见性这类业务语义。
|
||||
|
||||
### Controller 层
|
||||
|
||||
- Controller 负责 HTTP 暴露、参数校验、权限注解、结果封装。
|
||||
- 不要在 controller 中直接编排复杂业务流程,也不要直接操作多个 mapper 拼装业务规则。
|
||||
- 请求和响应对象优先沿用 `ReqVO`、`RespVO` 风格,不要直接把 DO 暴露给前端。
|
||||
|
||||
### Service 层
|
||||
|
||||
- 核心业务规则、事务、缓存、领域编排应落在 service 层。
|
||||
- 如果是已有领域增强,优先在现有 service 下扩展,不要为了“看起来更整齐”平移整套代码。
|
||||
- 不要把复杂规则散落到 controller、mapper 或 `util` 中。
|
||||
|
||||
### DAL 层
|
||||
|
||||
- 新表应有对应的 DO 和 Mapper。
|
||||
- Mapper 优先继承 `BaseMapperX<T>`,不要重复写样板 CRUD。
|
||||
- 查询条件优先沿用 `LambdaQueryWrapperX`、默认方法封装和现有 MyBatis Plus 风格,不要无必要回退到 XML。
|
||||
- Mapper 以查询封装为主,不承担领域校验职责。
|
||||
|
||||
### Convert 层
|
||||
|
||||
- 如果某个领域已经有 `convert` 风格,则继续沿用。
|
||||
- 简单场景允许直接使用 `BeanUtils`。
|
||||
- 不要为了统一而强推所有地方都改成 MapStruct,也不要反过来把已有 convert 全部删掉。
|
||||
|
||||
## 认证与共享调用约束
|
||||
|
||||
- 默认沿用现有 OAuth2 / Token / `LoginUser` / `login-user` 透传主链,不要另造一套认证上下文体系。
|
||||
- 不要额外发明 ThreadLocal、Session 或自定义 header 体系替代当前登录态恢复方式。
|
||||
- 接口级权限判断默认沿用 `@PreAuthorize("@ss.hasPermission(...)")` 这条链路,不要绕开现有权限框架另起一套实现。
|
||||
- 跨模块、跨服务访问能力时,优先通过对应的 `*-api` 模块定义 API、DTO、常量和枚举。
|
||||
- 不要让外部模块直接依赖某个 `*-boot` 模块的 service 或 mapper。
|
||||
|
||||
## 数据与 SQL 约束
|
||||
|
||||
- 新增业务表的 DO 优先复用当前 `BaseDO` / 审计字段风格;除非表本身明确不需要逻辑删除,不要再引入另一套审计基类。
|
||||
- 不要假设运行时存在自动数据库迁移;如果代码依赖新表、新字段或新索引,必须同步补齐对应 SQL 与文档说明。
|
||||
- SQL 脚本应放在目标模块的 `src/main/resources/sql/...` 下,并保持可审阅、可单独执行、语义清晰。
|
||||
- 变更缓存、日志、审计相关逻辑时,优先沿用现有机制,不要绕开现有登录上下文、缓存约定和审计字段填充方式。
|
||||
|
||||
## 注释与编码
|
||||
|
||||
- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。
|
||||
- 不要为了省事删除原有有效注释,也不要添加无信息量的注释。
|
||||
- 写入中文内容时必须保持 UTF-8 编码,并自行检查中文显示是否正常;不要用“改成英文”规避乱码问题。
|
||||
- 使用 superpowers 产出的功能文档时,例如设计文档、实施计划、联调说明,除非用户明确要求,否则默认用中文落地;代码标识、文件路径、接口路径、SQL、命令保持原始技术标识,不做意译。
|
||||
|
||||
## 工作规则
|
||||
|
||||
1. 除非任务明确要求修改共享契约或 starter,否则优先进行有边界的模块内改动,避免跨模块扩散。
|
||||
2. 业务逻辑应放在对应业务模块的 `*-boot` 实现模块;可复用契约放在对应的 `*-api` 模块;可复用框架能力放在 `rdms-framework`。
|
||||
3. 除非任务本身就是环境配置调整,否则避免修改 `application-local.yaml` 和 `application-dev.yaml`。
|
||||
4. 将本地资源 YAML 视为可能带有机器环境差异的文件;修改前先检查 git 状态。
|
||||
5. 保持既有包结构约定不变:
|
||||
- 控制器放在 `controller`
|
||||
- 服务层放在 `service`
|
||||
- 持久层放在 `dal`
|
||||
- DTO/VO 转换放在 `convert`
|
||||
6. 当前系统域代码主要在 `rdms-system`,RDMS 核心交付域代码主要在 `rdms-project`,但这不是永久约束;新增业务能力时,先判断应该落在现有系统域内、现有项目交付域内,还是应建设为新的 `rdms-xxx` 业务模块。
|
||||
7. 新增共享能力时,优先扩展现有 `rdms-spring-boot-starter-*` 模块,不要在业务服务里重复堆配置。
|
||||
8. 修改跨模块使用的 API 时,需要同时更新提供方实现和对应的 `rdms-system-api` 或对应 `rdms-xxx-api` 契约。
|
||||
9. 除非用户明确要求,否则不执行任何编译、构建、测试、打包或其他会实际运行项目的命令,包括但不限于 `mvn`、启动命令和脚本。
|
||||
|
||||
## 测试指引
|
||||
|
||||
先定义验证方式,再实施修改。默认通过以下方式验证:
|
||||
|
||||
- 代码路径是否闭环,调用链是否与模块边界一致
|
||||
- 配置项、接口契约、权限标识、路由或资源注册是否前后一致
|
||||
- 改动范围是否控制在当前任务所需的最小集合内
|
||||
- 受影响的文档、SQL、配置或接口说明是否需要同步更新
|
||||
|
||||
如果任务影响了 Spring 配置、序列化、安全、路由、RPC 契约、MyBatis 行为或跨模块 API,一律明确说明哪些部分已静态检查、哪些部分尚未实际运行验证。
|
||||
|
||||
## 给后续 Agent 的说明
|
||||
|
||||
- 仓库中可能存在未提交的本地配置改动,不要覆盖与当前任务无关的编辑。
|
||||
- `docs/` 目录属于当前工作上下文的一部分,不是归档材料;做架构级修改前先查阅。
|
||||
- 根目录 `pom.xml` 负责统一版本和依赖对齐;涉及版本调整时,优先修改根 `pom.xml`,不要散落到子模块中。
|
||||
适用范围:以 `C:\code\gitea\rdms\cn-rdms` 为根目录的整个仓库。所有交互原则、本机环境、模块结构、分层职责、鉴权通道、HTTP 动词语义、数据与 SQL、注释编码、Git 纪律、验证默认动作等约束,请直接阅读 `CLAUDE.md`。
|
||||
|
||||
175
CLAUDE.md
Normal file
175
CLAUDE.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# CLAUDE.md
|
||||
|
||||
本文件为 `C:\code\gitea\rdms\cn-rdms` 仓库下所有 Agent(含 Claude Code)的常驻工作指引;`AGENTS.md` 引用本文件。
|
||||
|
||||
## 工作方式
|
||||
|
||||
- 默认先给执行方案:目标、涉及模块、改动点、验证方式;不擅自动手。
|
||||
- 用户只要分析或评审时,停在分析层;不要顺手开工。
|
||||
- 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。
|
||||
- **输出极简**:先给结论、改动点、必要风险;用自然语言给判断和影响面,少贴代码片段;涉及代码用 `file_path:line_number` 引用;用户追问再展开。
|
||||
- **下定论需要充足证据**。疑似 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 / 其他版本。
|
||||
- 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 17,Spring Boot 3.5.9,根模块打包 `pom`。
|
||||
|
||||
顶层模块:
|
||||
1. `rdms-system` — 系统域(用户/组织/岗位/菜单/角色/权限)
|
||||
2. `rdms-project` — RDMS 核心交付域(项目集/项目/产品/需求/任务/工单/执行)
|
||||
3. `rdms-framework` — 共享框架与内部 starter
|
||||
4. `rdms-gateway` — Spring Cloud Gateway 网关
|
||||
|
||||
每个业务模块按 `xxx-api` + `xxx-boot` 拆分:
|
||||
- `*-api`:对外 RPC/Feign 接口、DTO、错误码、枚举、常量
|
||||
- `*-boot`:启动类、controller、service、dal、convert、api 实现、模块级 framework 配置
|
||||
|
||||
主包/启动类:
|
||||
- `rdms-system-boot`:`com.njcn.rdms.module.system.SystemServerApplication`
|
||||
- `rdms-project-boot`:`com.njcn.rdms.module.project.ProjectServerApplication`
|
||||
- `rdms-gateway`:`com.njcn.rdms.gateway.GatewayServerApplication`
|
||||
|
||||
`rdms-framework` 子模块:`rdms-common` 及一组 `rdms-spring-boot-starter-*`(`env`、`web`、`rpc`、`security`、`mybatis`、`redis`、`mq`、`websocket`、`excel`、`protection`、`test`、`biz-ip`)。
|
||||
|
||||
## 模块演进判断
|
||||
|
||||
新增能力时**先判断落点**:
|
||||
- 落在现有 `rdms-system` 子域 → 沿用 `controller/admin|app`、`service`、`dal/dataobject`、`dal/mysql`、`convert` 的现有结构,跨模块暴露时在 `rdms-system-api` 补 API/DTO/错误码/枚举。
|
||||
- 落在现有 `rdms-project` 子域 → 同理,跨模块走 `rdms-project-api`。
|
||||
- 已具备独立服务边界(如未来的 `rdms-workflow`)→ 新建 `rdms-xxx` + `rdms-xxx-api` + `rdms-xxx-boot`,根 `pom.xml` 加聚合,包路径 / `spring.application.name` / `ApiConstants` / `RpcConstants` / `rdms.info.base-package` 保持一致。
|
||||
|
||||
不要:
|
||||
- 把后续业务长期堆进 `rdms-system`。
|
||||
- 为新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层。
|
||||
- 让外部模块直接依赖 `*-boot` 的 service 或 mapper(必须走 `*-api`)。
|
||||
|
||||
## 分层职责
|
||||
|
||||
| 层 | 职责 | 红线 |
|
||||
|---|---|---|
|
||||
| `rdms-framework` | 基础能力 | 不承载业务语义;除非框架级缺陷或全局基础设施,不要把业务判断塞进来 |
|
||||
| `rdms-gateway` | 入口、令牌校验、登录用户透传、路由、横切 | 不要在这里承载组织/成员/负责人/项目/产品/工作流状态/数据可见性 |
|
||||
| Controller | HTTP 暴露、参数校验、权限注解、结果封装 | 不要编排复杂业务流程,不要直接操作多个 mapper;用 `ReqVO`/`RespVO`,不要直接暴露 DO |
|
||||
| Service | 业务规则、事务、缓存、领域编排 | 已有领域优先扩展,不要为"整齐"平移;不要把规则散到 controller / mapper / util |
|
||||
| DAL | DO + Mapper | Mapper 继承 `BaseMapperX<T>`;查询优先 `LambdaQueryWrapperX` 与默认方法封装;非必要不回退 XML;不承担领域校验 |
|
||||
| Convert | 已有 `convert` 风格继续沿用,简单场景直接 `BeanUtils` | 不要强推全员 MapStruct,也不要反过来把已有 convert 全删 |
|
||||
|
||||
## 认证与跨模块调用
|
||||
|
||||
- 默认沿用 OAuth2 / Token / `LoginUser` / `login-user` 透传主链。**不要**另造 ThreadLocal / Session / 自定义 header。
|
||||
- 业务逻辑落 `*-boot`;可复用契约落 `*-api`;可复用框架能力落 `rdms-framework`。跨模块/跨服务必须通过 `*-api` 定义契约,**不要直接依赖别人的 `*-boot`**;改跨模块 API 时,`*-boot` 实现与对应 `*-api` 契约同步更新。
|
||||
|
||||
### 鉴权:必须按"全域 / 对象域"分通道挂
|
||||
|
||||
系统有**两条互不交叉**的权限通道,挂错通道 = 永远 403。新增/修改接口前必须先判断它属于哪一域:
|
||||
|
||||
| 通道 | 适用场景 | 注解 / 实现 | 角色与菜单 |
|
||||
|---|---|---|---|
|
||||
| **全域 global** | 传统 RBAC 顶层菜单与"项目管理界面"——选择对象**之前**的所有动作(建项目、列项目、菜单/角色/用户管理等) | Controller 上 `@PreAuthorize("@ss.hasPermission('xxx')")`,由 `PermissionServiceImpl.hasAnyPermissions` 处理 | `system_role.scope_type='global'` + `system_menu.scope_type='global'` |
|
||||
| **对象域 object** | 用户**已选择某个对象**(如某个项目/产品)后,对象内部的一切操作(任务、执行、工时、协办人、需求、成员维护等) | Service 上 `@CheckObjectPermission(objectType=..., objectId="#xxxId", permission="...")`,由 `ObjectPermissionAspect` → `ObjectPermissionService.checkPermission` 处理 | `system_role.scope_type='object'` + `system_menu.scope_type='object'` + `object_type` |
|
||||
|
||||
红线:
|
||||
|
||||
- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。
|
||||
- **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。
|
||||
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
|
||||
- 列表/详情这类对象内**读路径**目前未挂 `@CheckObjectPermission`(属已识别负债,台账 TD-001),新增读接口暂沿用现状即可,不要顺手改造,等独立立项。
|
||||
|
||||
判定口诀:**URL 里有 `{projectId}` / `{productId}` 等对象 ID → 对象域;没有 → 全域**。
|
||||
|
||||
## 接口语义(HTTP 动词)
|
||||
|
||||
本仓库 update 类接口默认按 RESTful 标准用 HTTP 动词区分语义,前后端必须按下表对齐,避免"前端没传字段"和"前端想清空"在后端无法区分的歧义。
|
||||
|
||||
| 动词 | 语义 | 字段处理规则 |
|
||||
|---|---|---|
|
||||
| **PUT** | 全资源替换 | 前端必须把表单完整状态回传(读到的非必填字段也要原样回传)。后端按字段值落库:**有值=更新,`null`=清空**。DO 字段加 `@TableField(updateStrategy = FieldStrategy.ALWAYS)` 跳过全局 `NOT_NULL` 让 `null` 真的写库 |
|
||||
| **PATCH** | 部分字段更新 | **本仓库暂不引入 PATCH 接口**。如果有"只改一两个字段"的需求,用专门的子动作接口(参考 `assignees/{id}/inactive`、`status` 这种语义化路径),不要在 update 接口里靠旁路标记(如 `clearXxx: true`)模拟 PATCH |
|
||||
| **DELETE** | 资源删除 | 软删走全局 `deleted` 列,不需要参数 body |
|
||||
|
||||
红线:
|
||||
|
||||
- **不要在 update 类接口的 ReqVO 里加 `clearXxx: Boolean` 这种旁路标记**来模拟 PATCH —— 等于承认接口是"伪 PATCH",会让所有非必填字段都需要类似标记,长期污染 API 设计。需要部分更新就拆子动作接口。
|
||||
- 新增 update 接口时,必须在 API 文档对应章节明示"PUT 全字段回传"约定;DO 上对允许 null 的字段补 `FieldStrategy.ALWAYS` 注解,并加注释说明语义来源(指向本节)。
|
||||
- 历史接口若是稀疏 PATCH 风格(传 null = 不动),保留现状但不要拓展;遇到清空诉求时按 PUT 方向重构。
|
||||
|
||||
## 数据与 SQL
|
||||
|
||||
- 新表 DO 复用现有 `BaseDO` / 审计字段风格,不要再引一套审计基类(除非该表本身明确不需要逻辑删除)。
|
||||
- **不要假设运行时自动数据库迁移**:依赖新表/新字段/新索引时,必须同步补 SQL 脚本与文档。
|
||||
- SQL 放在目标模块 `src/main/resources/sql/...`,可审阅、可单独执行。
|
||||
- 缓存/日志/审计变更优先沿用既有机制,不要绕开登录上下文与审计字段填充。
|
||||
|
||||
### 种子 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`。
|
||||
|
||||
- **id 取值**:`SET @new_id = (SELECT IFNULL(MAX(id), 0) + 1 FROM xxx_table);` 然后 INSERT `SELECT @new_id, ...`。雪花 ID 单调递增,`MAX+1` 落在已用区间之后,不会与未来 Java 生成的新雪花 ID 冲突。
|
||||
- **多条连续 INSERT**:每条 INSERT **前重新取** `MAX+1`——不要用 `base+1 / +2 / +3` 一次性算多个。配合 `NOT EXISTS` 守卫,部分已存在场景(半路重跑)才不会出现两条共用一个 id。
|
||||
- **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 隐式解析,不冲突。
|
||||
|
||||
样板参考:`docs/sql/rdms_task_worklog.sql:47-50`(菜单种子)+ `docs/sql/rdms_worklog_difficulty_seed.sql`(字典种子)。
|
||||
|
||||
## 注释与编码
|
||||
|
||||
- 关键字段/分支/约束/非直观实现补**简洁中文**注释;中文写入必须 UTF-8,不要用"改成英文"规避乱码。
|
||||
- superpowers 产出的功能文档(设计/实施/联调)默认中文落地;代码标识、文件路径、接口路径、SQL、命令保持原样不意译。
|
||||
|
||||
## 文档输出格式
|
||||
|
||||
- 新写文档默认输出 **HTML 格式**(便于浏览器直接打开、自带样式)。
|
||||
- 例外:`docs/superpowers/` 下保持 markdown(工作流约定)。
|
||||
- 历史已有的 markdown 文档不强制迁移;只有新写的按 HTML。
|
||||
|
||||
## 工作规则(执行前对照)
|
||||
|
||||
1. 优先做有边界的模块内改动,避免跨模块扩散。
|
||||
2. **不要修改** `application-local.yaml` / `application-dev.yaml`,除非任务本身就是环境配置调整;改前先查 git 状态。
|
||||
3. 新增共享能力优先扩展现有 `rdms-spring-boot-starter-*`,不要在业务服务里重复堆配置。
|
||||
4. **未经用户明确同意,不执行任何 `mvn`、启动命令、脚本等会实际运行项目的命令。**
|
||||
|
||||
## Git 操作纪律
|
||||
|
||||
### 默认不引导分支管理(**首要**)
|
||||
|
||||
用户在本仓库长期固定在 `main` 上工作。开发流程中:
|
||||
|
||||
- **不要主动建议建 feature 分支**(`git checkout -b feat/xxx`、`git switch -c ...`)。
|
||||
- **不要把"先切到 xxx 分支再操作"作为方案前置步骤**。
|
||||
- 一切围绕 `main` 展开:直接在 `main` 上改、`main` 上提交、`main` 上推。
|
||||
- 例外:用户明确要求建分支、或涉及多人协作 / PR 评审 / 大规模重构(此时仍只是"提一句作为可选",不强推)。
|
||||
|
||||
**理由**:这次差点丢用户 3 天工作的事故,根因就是分支管理本身——某次操作意外把文件名当成分支名(建出 `用户行动清单.md` 分支),后续"切回 main + `git branch -D` 删怪分支"流程里就把未推送 commit `8bad989` 干掉了。**少走分支 = 少埋雷**。
|
||||
|
||||
### 破坏性 git 命令必须先核实
|
||||
|
||||
任何**会丢工作**的 git 命令——`branch -D`、`reset --hard`、`clean -fd`、`push --force` / `--force-with-lease`、`checkout` / `switch` 带未提交改动、`rebase` 在已推送分支上、直接动 `.git/` 内部文件(`refs/`、`HEAD`、`packed-refs`)——**给出建议前必须先核实**,不得凭"看起来安全"就甩命令:
|
||||
|
||||
1. **目标 ref 上是否有未推送 / 未合并 commit**:让用户跑 `git log --oneline -5 <ref>` 或 `git log <主线>..<ref>` 把输出贴回来。
|
||||
2. **工作区是否干净**:`git status`。
|
||||
3. **先挂救生圈**:建议用 `git tag backup-xxx <sha>` 锁定当前 SHA,**再执行**破坏性命令。
|
||||
4. **明示翻车回滚路径**:例如"如果不对,`git reset --hard backup-xxx` 即可回到此处"。
|
||||
|
||||
## 验证默认动作
|
||||
|
||||
先定义验证方式,再做修改。默认静态验证:
|
||||
- 调用链是否闭环、是否符合模块边界
|
||||
- 配置项 / 接口契约 / 权限标识 / 路由 / 资源注册前后是否一致
|
||||
- 改动是否控制在最小集合
|
||||
- 文档 / SQL / 配置 / 接口说明是否需要同步更新
|
||||
|
||||
如果改动涉及 Spring 配置、序列化、安全、路由、RPC 契约、MyBatis 行为或跨模块 API,必须**显式区分**哪些是已静态检查、哪些尚未实际运行验证。
|
||||
|
||||
## 给后续我自己的提醒
|
||||
|
||||
- 仓库可能有未提交的本地改动,不要顺手覆盖与当前任务无关的编辑。
|
||||
- `docs/` 是当前工作上下文的一部分,不是归档;架构级修改前先查阅 [`docs/README.md`](./docs/README.md)。
|
||||
- 根 `pom.xml` 统一版本与依赖;版本调整改根 pom,不要散落到子模块。
|
||||
- 推荐使用 `Glob` / `Grep` / `Read` 等专用工具,避免用 Bash 做文件搜索/读取/编辑。
|
||||
355
docs/superpowers/specs/2026-05-22-ticket-design.md
Normal file
355
docs/superpowers/specs/2026-05-22-ticket-design.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# 工单需求规格说明
|
||||
|
||||
日期: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` 资源入口,后端接口落地时需要与前端路由和菜单权限同步。
|
||||
|
||||
1
pom.xml
1
pom.xml
@@ -29,6 +29,7 @@
|
||||
<spring.boot.version>3.5.9</spring.boot.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<swagger.version>2.2.38</swagger.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
<!-- Swagger 注解,用于 API 文档生成(@Schema、@Operation 等) -->
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<artifactId>swagger-annotations-jakarta</artifactId>
|
||||
<version>${swagger.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 远程调用相关 -->
|
||||
@@ -180,4 +181,4 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -27,9 +27,8 @@ public class PageParam implements Serializable {
|
||||
@Min(value = 1, message = "页码最小值为 1")
|
||||
private Integer pageNo = PAGE_NO;
|
||||
|
||||
@Schema(description = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
@Schema(description = "每页条数,最大值为 200;传 -1 表示不分页(查询全部)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
@NotNull(message = "每页条数不能为空")
|
||||
@Min(value = 1, message = "每页条数最小值为 1")
|
||||
@Max(value = 200, message = "每页条数最大值为 200")
|
||||
private Integer pageSize = PAGE_SIZE;
|
||||
|
||||
|
||||
@@ -99,7 +99,8 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
|
||||
List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> StrUtil.isEmpty(EnvUtils.getTag(instance)));
|
||||
// 【重要】补充说明:如果希望在 chooseInstances 为空时,不允许打到有 tag 的实例,可以取消注释下面的代码
|
||||
if (CollUtil.isEmpty(chooseInstances)) {
|
||||
log.warn("[filterTagServiceInstances][serviceId({}) 没有不带 tag 的服务实例列表,直接使用所有服务实例列表]", serviceId);
|
||||
// 本地开发场景下所有实例都带 tag(HOSTNAME),fallback 到全集属于设计内常态、请求并未失败,降级为 debug 避免噪音
|
||||
log.debug("[filterTagServiceInstances][serviceId({}) 没有不带 tag 的服务实例列表,直接使用所有服务实例列表]", serviceId);
|
||||
chooseInstances = instances;
|
||||
}
|
||||
return chooseInstances;
|
||||
@@ -108,7 +109,8 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
|
||||
// 情况二,有 tag 时,使用 tag 匹配服务实例
|
||||
List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance)));
|
||||
if (CollUtil.isEmpty(chooseInstances)) {
|
||||
log.warn("[filterTagServiceInstances][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
|
||||
// 同上:未命中 tag 时 fallback 到全集是设计内正常路径,降级为 debug
|
||||
log.debug("[filterTagServiceInstances][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
|
||||
chooseInstances = instances;
|
||||
}
|
||||
return chooseInstances;
|
||||
|
||||
@@ -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.WebFrameworkUtils;
|
||||
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.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
@@ -25,10 +26,12 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@@ -37,6 +40,18 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private static final LoginUser LOGIN_USER_EMPTY = new LoginUser();
|
||||
|
||||
/**
|
||||
* 跳过 access token 校验的路径白名单。
|
||||
* 这些接口在 system 端标注 @PermitAll,本就不需要登录态;若前端调用时带过期 access,
|
||||
* 网关不应在此处拦截 1002023000,否则 /refresh-token 永远走不到 system 的 1002023001 / 业务逻辑。
|
||||
*/
|
||||
private static final Set<String> SKIP_AUTH_PATHS = Set.of(
|
||||
"/admin-api/system/auth/login",
|
||||
"/admin-api/system/auth/logout",
|
||||
"/admin-api/system/auth/refresh-token",
|
||||
"/admin-api/system/auth/register"
|
||||
);
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
private final LoadingCache<String, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1),
|
||||
@@ -44,8 +59,16 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public LoginUser load(String token) {
|
||||
String body = checkAccessToken(token).block();
|
||||
return buildUser(body, token);
|
||||
// 仅异步 refresh 走这里(同步链路用 getIfPresent + 直接 checkAccessToken,不触发 load)
|
||||
// 远端 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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -58,6 +81,11 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
exchange = SecurityFrameworkUtils.removeLoginUser(exchange);
|
||||
|
||||
// 白名单路径直接放行,不做 token 校验
|
||||
if (SKIP_AUTH_PATHS.contains(exchange.getRequest().getPath().value())) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
String token = SecurityFrameworkUtils.obtainAuthorization(exchange);
|
||||
if (StrUtil.isEmpty(token)) {
|
||||
return chain.filter(exchange);
|
||||
|
||||
@@ -6,10 +6,10 @@ spring:
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
config: # 【注册中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
|
||||
#################### 监控相关配置 ####################
|
||||
|
||||
@@ -6,10 +6,10 @@ spring:
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
config: # 【注册中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
|
||||
#################### 监控相关配置 ####################
|
||||
|
||||
@@ -30,6 +30,15 @@ spring:
|
||||
gateway:
|
||||
server:
|
||||
webflux:
|
||||
# HttpClient 连接池配置:网关作为反向代理客户端,复用到下游服务的 keep-alive 连接。
|
||||
# 必须保证 max-idle-time < 下游 server.tomcat.keep-alive-timeout(当前下游为 60s),
|
||||
# 否则服务端先 FIN、网关池仍持有"已死连接",复用时会抛 reactor.netty.http.client.PrematureCloseException。
|
||||
httpclient:
|
||||
connect-timeout: 10000 # 建立连接超时,毫秒
|
||||
response-timeout: 30s # 接收响应超时
|
||||
pool:
|
||||
max-idle-time: 30s # 闲置连接最长保留 30s,严格小于下游 keep-alive-timeout(60s)
|
||||
evict-in-background: 60s # 周期后台驱逐过期连接,进一步降低 race 概率
|
||||
# 路由配置项,对应 RouteDefinition 数组
|
||||
routes:
|
||||
## system-server 服务
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
# 产品动态时间线后端接口需求说明
|
||||
|
||||
日期:2026-04-23
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前产品对象首页中的“产品动态时间线”模块,用户期望不是展示几条前端拼装的摘要,而是一个可以在首页内直接查询的正式动态模块。
|
||||
|
||||
本轮已确认的目标能力包括:
|
||||
|
||||
- 首页内直接展示产品动态时间线
|
||||
- 默认查询最近 `30 天`
|
||||
- 支持自定义起止时间筛选
|
||||
- 支持事件类型多选筛选
|
||||
- 支持分页
|
||||
- 第一版事件范围收敛在产品对象与团队关系,不混入需求池事件
|
||||
|
||||
## 2. 当前前端现状
|
||||
|
||||
当前前端可直接使用的真实接口只有:
|
||||
|
||||
- `GET /project/product/get`
|
||||
- `GET /project/product/{id}/settings`
|
||||
- `GET /project/product/{id}/members`
|
||||
|
||||
这些接口目前只能提供:
|
||||
|
||||
- 产品当前状态
|
||||
- 最近一次状态原因
|
||||
- 产品经理
|
||||
- 成员加入时间
|
||||
- 成员退出时间
|
||||
- 当前成员角色
|
||||
|
||||
它们可以勉强拼出少量“最近动态摘要”,但不足以支撑正式时间线查询模块。
|
||||
|
||||
## 3. 当前接口为什么不够
|
||||
|
||||
如果继续只依赖现有接口,前端存在以下硬缺口:
|
||||
|
||||
1. 没有统一的产品动态分页接口
|
||||
2. 没有事件类型维度,无法支持类型筛选
|
||||
3. 没有统一发生时间字段集合,无法稳定支持时间筛选
|
||||
4. 没有事件操作人字段,无法明确展示“谁做了什么”
|
||||
5. 没有统一的事件摘要字段,前端只能自己硬拼文案
|
||||
6. 没有产品状态变更前后值,无法展示“从什么状态变更为什么状态”
|
||||
7. 没有产品经理变更前后值,无法展示交接关系
|
||||
8. 没有分页总数字段,无法做首页内翻页
|
||||
|
||||
因此,现有接口只适合做“概览摘要”,不适合做“可查询产品动态时间线”。
|
||||
|
||||
## 4. 后端接口交付要求
|
||||
|
||||
后端必须新增一条专用分页接口:
|
||||
|
||||
`GET /project/product/{id}/activities/page`
|
||||
|
||||
这条接口只服务“产品动态时间线”能力,不承担需求池动态,不承担首页其它概览指标。
|
||||
|
||||
前端要求后端单独提供这条接口,原因如下:
|
||||
|
||||
- 语义清晰,前后端都容易维护
|
||||
- 首页时间线可直接消费
|
||||
- 后续如果要做独立的产品动态页,也可以继续复用这条接口
|
||||
- 不需要继续让前端从多个接口里拼装事件
|
||||
|
||||
## 5. 第一版事件范围
|
||||
|
||||
第一版事件类型确认收敛为 5 类:
|
||||
|
||||
- `product_created`:产品创建
|
||||
- `product_status_changed`:产品状态变更
|
||||
- `product_manager_changed`:产品经理变更
|
||||
- `product_member_joined`:成员加入
|
||||
- `product_member_removed`:成员移出
|
||||
|
||||
第一版明确不纳入:
|
||||
|
||||
- 成员角色调整
|
||||
- 需求新增
|
||||
- 需求状态流转
|
||||
- 需求关闭
|
||||
- 里程碑事件
|
||||
- 风险点事件
|
||||
|
||||
## 6. 查询参数要求
|
||||
|
||||
接口必须支持以下查询参数:
|
||||
|
||||
- `pageNo`:页码
|
||||
- `pageSize`:每页数量
|
||||
- `startTime`:开始时间
|
||||
- `endTime`:结束时间
|
||||
- `types`:事件类型数组,多选
|
||||
|
||||
补充要求:
|
||||
|
||||
- 当前端未传时间范围时,后端默认按最近 `30 天` 返回
|
||||
- 返回结果按 `occurredAt desc` 倒序排列
|
||||
- `types` 支持多选,不要求前端单选
|
||||
|
||||
示例:
|
||||
|
||||
```http
|
||||
GET /project/product/1001/activities/page?pageNo=1&pageSize=10&startTime=2026-03-24 00:00:00&endTime=2026-04-23 23:59:59&types=product_status_changed&types=product_manager_changed
|
||||
```
|
||||
|
||||
## 7. 返回结构要求
|
||||
|
||||
接口返回必须支持分页,分页结构至少应满足:
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 128,
|
||||
"list": [
|
||||
{
|
||||
"id": "act_001",
|
||||
"type": "product_status_changed",
|
||||
"title": "产品状态变更",
|
||||
"operatorId": "10001",
|
||||
"operatorName": "张敏",
|
||||
"occurredAt": "2026-04-23 10:32:15",
|
||||
"summary": "产品状态由暂停变更为启用",
|
||||
"reason": "测试恢复",
|
||||
"beforeStatus": "paused",
|
||||
"afterStatus": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
分页顶层字段至少包括:
|
||||
|
||||
- `total`
|
||||
- `list`
|
||||
|
||||
如果后端已有统一分页模型,可以沿用现有分页结构,但前端必须能稳定拿到总数和列表。
|
||||
|
||||
## 8. 事件通用字段要求
|
||||
|
||||
无论哪种事件类型,后端都应统一返回以下字段:
|
||||
|
||||
- `id`:事件唯一 ID
|
||||
- `type`:事件类型编码
|
||||
- `title`:事件标题
|
||||
- `operatorId`:操作人 ID
|
||||
- `operatorName`:操作人名称
|
||||
- `occurredAt`:发生时间
|
||||
- `summary`:事件摘要
|
||||
- `reason`:原因或备注,可为空
|
||||
|
||||
这些字段是首页时间线最小可展示集合。
|
||||
|
||||
## 9. 各事件类型的专属字段要求
|
||||
|
||||
### 9.1 产品创建 `product_created`
|
||||
|
||||
必须补充:
|
||||
|
||||
- `creatorUserId`
|
||||
- `creatorUserName`
|
||||
|
||||
### 9.2 产品状态变更 `product_status_changed`
|
||||
|
||||
这是当前最关键的一类事件,后端必须返回:
|
||||
|
||||
- `beforeStatus`
|
||||
- `afterStatus`
|
||||
- `reason`
|
||||
|
||||
前端需要用这组字段明确表达:
|
||||
|
||||
- 变更前状态
|
||||
- 变更后状态
|
||||
- 本次变更原因
|
||||
|
||||
例如:
|
||||
|
||||
- `暂停 -> 启用`
|
||||
- `启用 -> 归档`
|
||||
|
||||
### 9.3 产品经理变更 `product_manager_changed`
|
||||
|
||||
必须返回:
|
||||
|
||||
- `beforeManagerUserId`
|
||||
- `beforeManagerUserName`
|
||||
- `afterManagerUserId`
|
||||
- `afterManagerUserName`
|
||||
- `reason`
|
||||
|
||||
否则前端无法准确展示交接关系,只能看到当前经理,不能看到变更前后关系。
|
||||
|
||||
### 9.4 成员加入 `product_member_joined`
|
||||
|
||||
必须返回:
|
||||
|
||||
- `memberUserId`
|
||||
- `memberUserName`
|
||||
- `roleId`
|
||||
- `roleName`
|
||||
- `remark`
|
||||
|
||||
### 9.5 成员移出 `product_member_removed`
|
||||
|
||||
必须返回:
|
||||
|
||||
- `memberUserId`
|
||||
- `memberUserName`
|
||||
- `roleId`
|
||||
- `roleName`
|
||||
- `reason`
|
||||
|
||||
## 10. 前端展示口径
|
||||
|
||||
前端首页时间线模块第一版会直接基于这条接口支持:
|
||||
|
||||
- 默认最近 `30 天`
|
||||
- 自定义时间范围筛选
|
||||
- 事件类型多选筛选
|
||||
- 分页切换
|
||||
|
||||
每条记录最少展示:
|
||||
|
||||
- 事件类型
|
||||
- 事件标题
|
||||
- 操作人
|
||||
- 发生时间
|
||||
- 变更摘要
|
||||
- 原因/备注
|
||||
|
||||
其中“产品状态变更”需要明确体现:
|
||||
|
||||
- 变更前状态
|
||||
- 变更后状态
|
||||
- 变更原因
|
||||
|
||||
## 11. 为什么不建议继续让前端拼装
|
||||
|
||||
如果继续沿用当前前端拼装方案,会有这些问题:
|
||||
|
||||
- 产品状态变更前后值无法补齐
|
||||
- 产品经理变更前后值无法补齐
|
||||
- 无法支持分页
|
||||
- 无法支持统一时间筛选
|
||||
- 无法支持统一类型筛选
|
||||
- 不同事件文案会在前端散落拼装,长期维护成本高
|
||||
|
||||
因此这里的前后端边界应明确为:
|
||||
|
||||
- 后端提供统一产品动态分页接口
|
||||
- 前端负责筛选条件组织、分页交互和时间线展示
|
||||
|
||||
## 12. 本轮需求结论
|
||||
|
||||
本轮给后端的结论可以直接收敛为:
|
||||
|
||||
1. 当前前端已有接口不满足正式产品动态时间线需求
|
||||
2. 后端新增 `GET /project/product/{id}/activities/page`
|
||||
3. 接口必须支持分页、默认最近 `30 天`、自定义时间范围、事件类型多选
|
||||
4. 第一版事件类型只做:
|
||||
- 产品创建
|
||||
- 产品状态变更
|
||||
- 产品经理变更
|
||||
- 成员加入
|
||||
- 成员移出
|
||||
5. 产品状态变更必须提供前后状态和原因
|
||||
6. 产品经理变更必须提供前后经理信息
|
||||
|
||||
这条接口交付后,前端才能把当前“产品动态时间线”从拼装摘要升级成正式可查询模块。
|
||||
@@ -1,292 +0,0 @@
|
||||
# 产品对象首页改版设计说明
|
||||
|
||||
日期:2026-04-23
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本设计用于收敛 RDMS 产品对象上下文默认首页的改版方向。
|
||||
|
||||
本轮目标不是继续做“说明型占位页”,而是明确把当前 `/product/dashboard?objectId=...` 改成一个真正可用的产品对象首页:
|
||||
|
||||
- 第一眼先让用户知道当前看的是什么产品
|
||||
- 第二眼能快速判断对象最近发生了什么
|
||||
- 第三眼能看出需求池现在的经营状态和最近变化
|
||||
- 底部为后续业务模块保留正式挂载位,而不是临时拼接入口
|
||||
|
||||
## 2. 已确认诉求
|
||||
|
||||
基于本轮对话,已确认以下用户诉求:
|
||||
|
||||
1. 首页顶部必须先展示产品基础概述,而不是先铺统计卡片
|
||||
2. 基础概述至少包含:名称、编号、团队、产品经理等对象基础信息
|
||||
3. 页面需要一块明显的时间线,用于承接产品对象与团队变更动态
|
||||
4. 页面需要承接需求池管理情况,重点看总量、状态、待处理等统计信息
|
||||
5. 需求相关事件不要混入对象时间线,应单独作为需求池最近变化区域
|
||||
6. 快捷入口不要保留
|
||||
7. 底部允许保留后续扩展区,重点预留给里程碑、风险点管理、产品资料等模块
|
||||
8. 能接真实接口就接真实接口,当前没有稳定接口的区域允许先用假数据,但结构必须按正式首页来设计
|
||||
|
||||
## 3. 首页定位结论
|
||||
|
||||
本页定位不是:
|
||||
|
||||
- 纯报表看板
|
||||
- 纯审计日志页
|
||||
- 设置页搬运版
|
||||
- 导航入口集合页
|
||||
|
||||
本页定位应当是:
|
||||
|
||||
- 产品对象首页
|
||||
- 偏统计,也带审计
|
||||
- 但页面主语始终是“当前产品对象”
|
||||
|
||||
换句话说,这个页面要同时回答三个问题:
|
||||
|
||||
1. 我现在看的是什么产品?
|
||||
2. 这个产品对象最近发生了什么?
|
||||
3. 这个产品的需求池现在处于什么状态?
|
||||
|
||||
## 4. 页面结构
|
||||
|
||||
### 4.1 桌面端结构
|
||||
|
||||
桌面端建议采用三层结构:
|
||||
|
||||
1. 顶部 `对象基础概述横幅`
|
||||
2. 中部 `左时间线 + 右需求池双模块`
|
||||
3. 底部 `扩展信息区`
|
||||
|
||||
推荐布局比例:
|
||||
|
||||
- 顶部横幅:`24 / 24`
|
||||
- 中部主区:左 `16 / 24`,右 `8 / 24`
|
||||
- 底部扩展区:`24 / 24`
|
||||
|
||||
中部左侧时间线高度应明显高于右侧任一单模块,形成首页主阅读区。
|
||||
|
||||
### 4.2 移动端结构
|
||||
|
||||
移动端统一退化为单列纵向布局,顺序为:
|
||||
|
||||
1. 对象基础概述横幅
|
||||
2. 对象 / 团队动态时间线
|
||||
3. 需求池管理概览
|
||||
4. 需求池最近变化
|
||||
5. 扩展信息区
|
||||
|
||||
移动端不强撑左右栏并排,不做卡片墙式压缩。
|
||||
|
||||
## 5. 模块设计
|
||||
|
||||
### 5.1 对象基础概述横幅
|
||||
|
||||
顶部采用“档案横幅型”,不采用纯指标卡片型。
|
||||
|
||||
横幅左侧承接对象身份信息:
|
||||
|
||||
- 产品名称
|
||||
- 产品编号
|
||||
- 当前状态标签
|
||||
- 产品经理
|
||||
- 团队规模
|
||||
- 团队角色摘要
|
||||
- 简短描述或备注
|
||||
|
||||
横幅右侧承接 4 个摘要指标:
|
||||
|
||||
- 团队人数
|
||||
- 需求总量
|
||||
- 待处理需求
|
||||
- 最近动态时间
|
||||
|
||||
设计原则:
|
||||
|
||||
- 左侧负责建立对象识别
|
||||
- 右侧负责快速判断当前概况
|
||||
- 右侧指标只保留 4 项,不堆成报表卡片墙
|
||||
|
||||
### 5.2 对象 / 团队动态时间线
|
||||
|
||||
该区域位于中部左侧,是首页的主阅读区。
|
||||
|
||||
这条时间线只承接对象与团队变化,不承接需求事件。
|
||||
|
||||
第一版事件范围收敛为:
|
||||
|
||||
- 产品创建
|
||||
- 产品状态变更
|
||||
- 产品经理变更
|
||||
- 成员加入
|
||||
- 成员移出
|
||||
- 成员角色调整
|
||||
|
||||
每条时间线建议展示:
|
||||
|
||||
- 事件标题
|
||||
- 事件类型标签
|
||||
- 发生时间
|
||||
- 操作摘要
|
||||
- 必要时展示原因或备注
|
||||
|
||||
表达目标是“业务时间线”,不是后台审计表格。
|
||||
|
||||
### 5.3 需求池管理概览
|
||||
|
||||
该区域位于中部右侧上半块,用于表达需求池的经营状态。
|
||||
|
||||
第一版首页需要优先看到的内容:
|
||||
|
||||
- 需求总量
|
||||
- 各状态数量
|
||||
- 待处理数量
|
||||
- 高优先级待处理数量
|
||||
|
||||
展示方式建议为“摘要指标 + 状态分布列表”,不直接在首页展开完整需求表格。
|
||||
|
||||
这一块回答的是:
|
||||
|
||||
- 需求池是否健康
|
||||
- 当前待处理压力大不大
|
||||
- 是否存在需要优先关注的积压
|
||||
|
||||
### 5.4 需求池最近变化
|
||||
|
||||
该区域位于中部右侧下半块,与需求池管理概览上下分层,但属于同一侧栏语义。
|
||||
|
||||
该区域不重复展示总量,而是展示需求池最近发生的变化。
|
||||
|
||||
第一版建议承接:
|
||||
|
||||
- 最近新增需求
|
||||
- 最近状态流转
|
||||
- 最近关闭或完成
|
||||
|
||||
每条记录建议至少展示:
|
||||
|
||||
- 需求标题
|
||||
- 动作类型
|
||||
- 时间
|
||||
- 当前状态或状态变更摘要
|
||||
|
||||
若当前没有真实数据,仍保留正式模块壳,不退化成“待开发”一句话。
|
||||
|
||||
### 5.5 扩展信息区
|
||||
|
||||
底部不再保留快捷入口,改为正式扩展信息区。
|
||||
|
||||
当前优先预留 3 类模块位:
|
||||
|
||||
- 里程碑
|
||||
- 风险点管理
|
||||
- 产品资料
|
||||
|
||||
这一层的作用是:
|
||||
|
||||
- 为后续对象级信息继续扩展留下稳定挂载位
|
||||
- 不把中部主结构挤成信息大杂烩
|
||||
- 避免为了未来模块提前做假导航入口
|
||||
|
||||
如果当前没有稳定接口,可先保留正式卡片结构与空态说明。
|
||||
|
||||
## 6. 数据策略
|
||||
|
||||
### 6.1 真实接口优先
|
||||
|
||||
当前首页优先消费现有真实接口:
|
||||
|
||||
- `fetchGetProduct`
|
||||
- `fetchGetProductSettings`
|
||||
- `fetchGetProductMembers`
|
||||
|
||||
这些接口足以支撑:
|
||||
|
||||
- 对象基础概述中的名称、编号、状态、产品经理、描述
|
||||
- 团队人数与角色摘要
|
||||
- 最近动态中的产品创建、状态变化、成员加入/移出
|
||||
|
||||
### 6.2 假数据使用边界
|
||||
|
||||
当前没有稳定真实接口的区域,允许先用假数据,但边界必须明确:
|
||||
|
||||
- 需求池管理概览
|
||||
- 需求池最近变化
|
||||
- 扩展信息区中的里程碑、风险点管理、产品资料摘要
|
||||
|
||||
假数据的使用原则:
|
||||
|
||||
1. 只补“当前没有稳定接口”的区域
|
||||
2. 不反向污染对象基础信息
|
||||
3. 不把假数据混入对象上下文 store
|
||||
4. 数据源要集中放在概览页自己的 mock 模块中,方便后续替换
|
||||
|
||||
### 6.3 不推荐的做法
|
||||
|
||||
以下做法应避免:
|
||||
|
||||
- 把需求假数据散落写进页面组件
|
||||
- 用对象 demo 数据冒充真实产品详情
|
||||
- 把对象时间线和需求时间线混成一条
|
||||
- 用快捷入口伪装成首页内容
|
||||
|
||||
## 7. 空态规则
|
||||
|
||||
首页至少要区分三种状态:
|
||||
|
||||
1. 能力未接入,只能先显示正式占位信息
|
||||
2. 能力已接入,但当前该产品暂无业务数据
|
||||
3. 当前用户无权限查看该模块
|
||||
|
||||
这三种状态不能共用一套模糊文案。
|
||||
|
||||
对需求池和扩展信息区,当前阶段更推荐“正式空态”而不是“待开发”。
|
||||
|
||||
## 8. 页面边界
|
||||
|
||||
首页明确不承接以下内容:
|
||||
|
||||
- 快捷入口导航区
|
||||
- 完整团队成员表格
|
||||
- 完整需求列表表格
|
||||
- 设置页重表单
|
||||
- 完整审计日志明细页
|
||||
|
||||
首页要做的是概述、判断与阅读,不是重操作页。
|
||||
|
||||
## 9. 实施建议
|
||||
|
||||
第一阶段建议先完成结构性改造:
|
||||
|
||||
1. 重做顶部横幅,建立对象档案感
|
||||
2. 保留中部左高右双块结构
|
||||
3. 用真实接口接通对象概述与对象 / 团队时间线
|
||||
4. 用局部 mock 数据先接通需求池两块和底部扩展区
|
||||
|
||||
第二阶段再逐步替换需求池与扩展区数据源:
|
||||
|
||||
- 接真实需求池统计接口
|
||||
- 接真实需求动态接口
|
||||
- 接里程碑、风险点、产品资料摘要接口
|
||||
|
||||
## 10. 验证标准
|
||||
|
||||
本设计是否成立,可按以下标准判断:
|
||||
|
||||
1. 进入首页后,第一眼能认出当前产品对象
|
||||
2. 用户能自然读到对象 / 团队最近发生了什么
|
||||
3. 右侧能快速判断需求池当前压力与最近变化
|
||||
4. 页面看起来像“对象首页”,而不是“普通后台卡片堆叠页”
|
||||
5. 当前没有真实接口的区域也保留正式结构,不显得像临时占位
|
||||
6. 后续新增里程碑、风险点管理、产品资料等能力时,不需要推翻整页结构
|
||||
|
||||
## 11. 本轮设计结论
|
||||
|
||||
本轮最终设计结论如下:
|
||||
|
||||
- 首页定位为“产品对象首页”,偏统计,也带审计,但不做纯报表页
|
||||
- 顶部采用档案横幅型,先立住对象身份信息
|
||||
- 中部左侧是高权重的对象 / 团队动态时间线
|
||||
- 中部右侧拆为“需求池管理概览 + 需求池最近变化”上下两块
|
||||
- 底部去掉快捷入口,改为正式扩展信息区
|
||||
- 当前有真实接口的模块优先接真实接口
|
||||
- 当前没有稳定接口的区域允许先用假数据,但必须隔离在概览页局部 mock 数据源中
|
||||
@@ -24,7 +24,8 @@
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<artifactId>swagger-annotations-jakarta</artifactId>
|
||||
<version>${swagger.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 参数校验 -->
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.njcn.rdms.framework.common.exception.ErrorCode;
|
||||
|
||||
/**
|
||||
* Project 错误码枚举类
|
||||
*
|
||||
* <p>
|
||||
* 产品管理当前使用 1-008-001-000 段。
|
||||
*/
|
||||
public interface ErrorCodeConstants {
|
||||
@@ -33,24 +33,198 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PRODUCT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_001_021, "删除确认口令不正确");
|
||||
ErrorCode PRODUCT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_001_022, "产品状态已发生变化,请刷新后重试");
|
||||
ErrorCode PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_001_023, "产品状态定义不存在或已停用");
|
||||
// 产品创建原子接口(/create-with-team)专用:初始团队相关校验
|
||||
ErrorCode PRODUCT_INITIAL_TEAM_MANAGER_REQUIRED = new ErrorCode(1_008_001_024, "初始团队必须包含产品经理");
|
||||
ErrorCode PRODUCT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_001_025, "初始团队成员存在重复");
|
||||
ErrorCode PRODUCT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_001_026, "初始团队中存在非法角色");
|
||||
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_MEMBER_USER_INVALID = new ErrorCode(1_008_001_029, "产品成员不是有效系统用户");
|
||||
// 批量新增(POST /project/product/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
||||
ErrorCode PRODUCT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_001_030, "请勿在批量列表中重复添加同一成员");
|
||||
ErrorCode PRODUCT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_001_031, "批量新增不允许指定为经理,请通过编辑成员调整");
|
||||
// 批量移出(POST /project/product/{id}/members/batch/inactive)专用:同一请求内 memberId 重复
|
||||
ErrorCode PRODUCT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE = new ErrorCode(1_008_001_032, "请勿在批量移出列表中重复指定同一成员");
|
||||
|
||||
// ========== 产品需求 1-008-002-000 ==========
|
||||
ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_001, "当前需求状态不支持动作【{}】");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_002, "动作【{}】必须填写原因");
|
||||
ErrorCode REQUIREMENT_STATUS_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_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_006, "需求状态定义不存在或已停用");
|
||||
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_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待认领、待评审、待指派状态的需求才能删除");
|
||||
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_CANCEL = new ErrorCode(1_008_002_017, "只有不存在子需求,或子需求都处于已取消和已拒绝的状态才能取消");
|
||||
ErrorCode REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_002_013, "存在子需求,请先删除子需求");
|
||||
ErrorCode REQUIREMENT_MODULE_NOT_EXISTS = new ErrorCode(1_008_002_009, "需求模块不存在");
|
||||
ErrorCode REQUIREMENT_MODULE_NAME_DUPLICATE = new ErrorCode(1_008_002_010, "已经存在名称为【{}】的模块");
|
||||
ErrorCode REQUIREMENT_MODULE_NOT_BELONG_TO_PRODUCT = new ErrorCode(1_008_002_011, "模块不属于当前产品");
|
||||
ErrorCode REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS = new ErrorCode(1_008_002_012, "模块下存在非终态需求,不可删除");
|
||||
ErrorCode REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_002_013, "存在子需求,请先删除子需求");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待确认、待评审、待分流状态的需求才能删除");
|
||||
ErrorCode REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_002_015, "存在子模块,请先删除子模块");
|
||||
ErrorCode REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_002_016, "模块下存在需求,请先删除需求");
|
||||
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_NOT_DISPATCHED = new ErrorCode(1_008_002_020, "该产品需求尚未指派到关联项目");
|
||||
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, "您不是该项目的成员,无权访问");
|
||||
|
||||
// ========== 项目管理 1-008-002-000 ==========
|
||||
ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1_008_002_000, "项目不存在");
|
||||
ErrorCode PROJECT_CODE_DUPLICATE = new ErrorCode(1_008_002_001, "已经存在编码为【{}】的项目");
|
||||
ErrorCode PROJECT_NAME_DUPLICATE = new ErrorCode(1_008_002_002, "当前产品下已经存在名称为【{}】的项目");
|
||||
ErrorCode PROJECT_CODE_NOT_MODIFIABLE = new ErrorCode(1_008_002_003, "项目编码创建后不允许修改");
|
||||
ErrorCode PROJECT_PRODUCT_NOT_MODIFIABLE = new ErrorCode(1_008_002_004, "项目所属产品第一期不允许直接修改");
|
||||
ErrorCode PROJECT_PRODUCT_NOT_EXISTS = new ErrorCode(1_008_002_005, "所属产品不存在或不可用");
|
||||
ErrorCode PROJECT_TYPE_INVALID = new ErrorCode(1_008_002_006, "项目类型不是有效字典值");
|
||||
ErrorCode PROJECT_MANAGER_USER_INVALID = new ErrorCode(1_008_002_007, "项目负责人不是有效系统用户");
|
||||
ErrorCode PROJECT_MEMBER_USER_INVALID = new ErrorCode(1_008_002_008, "项目成员不是有效系统用户");
|
||||
ErrorCode PROJECT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_009, "当前项目状态不支持动作【{}】");
|
||||
ErrorCode PROJECT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_010, "动作【{}】必须填写原因");
|
||||
ErrorCode PROJECT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_011, "项目状态已发生变化,请刷新后重试");
|
||||
ErrorCode PROJECT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_012, "当前项目状态不允许编辑");
|
||||
ErrorCode PROJECT_MEMBER_NOT_EXISTS = new ErrorCode(1_008_002_013, "项目成员不存在");
|
||||
ErrorCode PROJECT_MEMBER_ALREADY_EXISTS = new ErrorCode(1_008_002_014, "该用户已是当前项目的有效成员");
|
||||
ErrorCode PROJECT_MEMBER_ROLE_INVALID = new ErrorCode(1_008_002_015, "角色不存在或不属于项目对象角色");
|
||||
ErrorCode PROJECT_MANAGER_MEMBER_NOT_ALLOW_REMOVE = new ErrorCode(1_008_002_016, "当前项目负责人不能移出项目团队,请先完成负责人变更");
|
||||
ErrorCode PROJECT_MEMBER_NOT_ACTIVE = new ErrorCode(1_008_002_017, "当前项目成员已失效");
|
||||
ErrorCode PROJECT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_002_018, "删除确认口令不正确");
|
||||
ErrorCode PROJECT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_002_019, "删除确认名称与当前项目名称不一致");
|
||||
ErrorCode PROJECT_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_020, "当前项目不允许删除");
|
||||
ErrorCode PROJECT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_002_021, "当前用户不具备该项目的操作权限【{}】");
|
||||
ErrorCode PROJECT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_022, "项目状态定义不存在或已停用");
|
||||
ErrorCode PROJECT_DIRECTION_INVALID = new ErrorCode(1_008_002_023, "项目方向不是有效字典值");
|
||||
ErrorCode PROJECT_MANAGER_TRANSFER_INFO_REQUIRED = new ErrorCode(1_008_002_024, "切换项目经理时必须同时传入原项目经理用户和交接后角色");
|
||||
ErrorCode PROJECT_MANAGER_TRANSFER_SOURCE_INVALID = new ErrorCode(1_008_002_025, "原项目经理信息与当前项目经理不一致");
|
||||
ErrorCode PROJECT_MANAGER_TRANSFER_ROLE_INVALID = new ErrorCode(1_008_002_026, "原项目经理交接后的角色不能仍为项目经理");
|
||||
ErrorCode PROJECT_MANAGER_MEMBER_NOT_ALLOW_DOWNGRADE = new ErrorCode(1_008_002_027, "当前项目经理不能直接调整为非经理角色,请先完成经理转交");
|
||||
ErrorCode PROJECT_MAINLINE_DUPLICATE = new ErrorCode(1_008_002_028, "当前产品下已存在未作废的主线项目");
|
||||
// 项目创建原子接口(/create-with-team)专用:初始团队 + 方向一致性校验
|
||||
ErrorCode PROJECT_INITIAL_TEAM_MANAGER_REQUIRED = new ErrorCode(1_008_002_029, "初始团队必须包含项目经理");
|
||||
ErrorCode PROJECT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_002_030, "初始团队成员存在重复");
|
||||
ErrorCode PROJECT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_002_031, "初始团队中存在非法角色");
|
||||
ErrorCode PROJECT_DIRECTION_NOT_MATCH_PRODUCT = new ErrorCode(1_008_002_032, "项目方向与所属产品方向不一致");
|
||||
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 找到,请联系管理员");
|
||||
// 批量新增(POST /project/project/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
||||
ErrorCode PROJECT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_002_035, "请勿在批量列表中重复添加同一成员");
|
||||
ErrorCode PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_002_036, "批量新增不允许指定为经理,请通过编辑成员调整");
|
||||
// 批量移出(POST /project/project/{id}/members/batch/inactive)专用:同一请求内 memberId 重复
|
||||
ErrorCode PROJECT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE = new ErrorCode(1_008_002_037, "请勿在批量移出列表中重复指定同一成员");
|
||||
|
||||
// ========== 执行管理 1-008-003-000 ==========
|
||||
ErrorCode PROJECT_EXECUTION_NOT_EXISTS = new ErrorCode(1_008_003_000, "执行不存在");
|
||||
ErrorCode PROJECT_EXECUTION_NAME_DUPLICATE = new ErrorCode(1_008_003_001, "当前项目下已经存在名称为【{}】的执行");
|
||||
ErrorCode PROJECT_EXECUTION_OWNER_INVALID = new ErrorCode(1_008_003_002, "执行负责人必须是当前项目的有效成员");
|
||||
ErrorCode PROJECT_EXECUTION_ASSIGNEE_INVALID = new ErrorCode(1_008_003_003, "执行协办人必须是当前项目的有效成员");
|
||||
ErrorCode PROJECT_EXECUTION_ASSIGNEE_ALREADY_EXISTS = new ErrorCode(1_008_003_004, "该用户已是当前执行的有效协办人");
|
||||
ErrorCode PROJECT_EXECUTION_ASSIGNEE_NOT_EXISTS = new ErrorCode(1_008_003_005, "执行协办人不存在");
|
||||
ErrorCode PROJECT_EXECUTION_ASSIGNEE_NOT_ACTIVE = new ErrorCode(1_008_003_006, "当前执行协办人已失效");
|
||||
// 保留:TD-013 解锁后业务路径已不会再触发,预留用于灰度回滚关闭关联能力
|
||||
ErrorCode PROJECT_EXECUTION_REQUIREMENT_NOT_READY = new ErrorCode(1_008_003_007, "当前阶段不支持给执行绑定项目需求");
|
||||
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_008, "当前项目状态不允许维护执行");
|
||||
ErrorCode PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED = new ErrorCode(1_008_003_009, "该项目成员仍担任未终态执行负责人,请先完成执行负责人交接");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_003_010, "执行状态定义不存在或已停用");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_003_011, "当前执行状态不支持动作【{}】");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_003_012, "动作【{}】必须填写原因");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_003_013, "执行状态已发生变化,请刷新后重试");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_014, "当前执行状态不允许维护执行");
|
||||
ErrorCode PROJECT_EXECUTION_TYPE_INVALID = new ErrorCode(1_008_003_015, "执行类型不是有效字典值");
|
||||
ErrorCode PROJECT_EXECUTION_ASSIGNEE_REQUIRED = new ErrorCode(1_008_003_016, "创建执行时必须至少选择一名执行协办人");
|
||||
ErrorCode PROJECT_EXECUTION_STATUS_OWNER_ONLY = new ErrorCode(1_008_003_017, "只有执行负责人才能执行【{}】动作");
|
||||
ErrorCode PROJECT_EXECUTION_COMPLETE_TASKS_REQUIRED = new ErrorCode(1_008_003_018, "完成执行前,执行下所有任务必须全部完成或取消");
|
||||
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_DELETE = new ErrorCode(1_008_003_019, "已完成的执行不允许删除");
|
||||
ErrorCode PROJECT_EXECUTION_DELETE_NAME_MISMATCH = new ErrorCode(1_008_003_020, "确认执行名称与实际不一致");
|
||||
ErrorCode PROJECT_EXECUTION_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_003_021, "删除确认口令必须为 DELETE 或 删除");
|
||||
ErrorCode PROJECT_EXECUTION_PRIORITY_INVALID = new ErrorCode(1_008_003_022, "执行优先级不是有效字典值");
|
||||
ErrorCode PROJECT_EXECUTION_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_003_023, "关联的项目需求不存在或已删除");
|
||||
ErrorCode PROJECT_EXECUTION_REQUIREMENT_NOT_BELONG_TO_PROJECT = new ErrorCode(1_008_003_024, "关联的项目需求不属于当前项目");
|
||||
ErrorCode PROJECT_EXECUTION_REQUIREMENT_TERMINAL = new ErrorCode(1_008_003_025, "项目需求已处于终态,不允许关联新执行");
|
||||
|
||||
// ========== 任务管理 1-008-004-000 ==========
|
||||
ErrorCode PROJECT_TASK_NOT_EXISTS = new ErrorCode(1_008_004_000, "任务不存在");
|
||||
ErrorCode PROJECT_TASK_OWNER_INVALID = new ErrorCode(1_008_004_001, "任务负责人必须是当前执行的有效成员");
|
||||
ErrorCode PROJECT_TASK_PARENT_INVALID = new ErrorCode(1_008_004_002, "父任务必须属于当前项目和执行");
|
||||
ErrorCode PROJECT_TASK_NOT_ALLOW_EDIT = new ErrorCode(1_008_004_003, "当前项目或执行状态不允许维护任务");
|
||||
ErrorCode PROJECT_TASK_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_004_004, "任务状态定义不存在或已停用");
|
||||
ErrorCode PROJECT_TASK_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_004_005, "当前任务状态不支持动作【{}】");
|
||||
ErrorCode PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_004_006, "动作【{}】必须填写原因");
|
||||
ErrorCode PROJECT_TASK_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_004_007, "任务状态已发生变化,请刷新后重试");
|
||||
ErrorCode PROJECT_TASK_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_004_008, "当前任务状态不允许维护任务");
|
||||
ErrorCode PROJECT_TASK_COMPLETE_CHILDREN_REQUIRED = new ErrorCode(1_008_004_010, "父任务完成前,子任务必须全部完成或取消");
|
||||
ErrorCode PROJECT_TASK_STATUS_OWNER_ONLY = new ErrorCode(1_008_004_011, "只有任务负责人才能执行【{}】动作");
|
||||
ErrorCode PROJECT_TASK_NOT_ALLOW_DELETE = new ErrorCode(1_008_004_012, "已完成的任务不允许删除");
|
||||
ErrorCode PROJECT_TASK_DELETE_NAME_MISMATCH = new ErrorCode(1_008_004_013, "确认任务名称与实际不一致");
|
||||
ErrorCode PROJECT_TASK_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_004_014, "删除确认口令必须为 DELETE 或 删除");
|
||||
ErrorCode PROJECT_TASK_PRIORITY_INVALID = new ErrorCode(1_008_004_015, "任务优先级不是有效字典值");
|
||||
ErrorCode PROJECT_TASK_LEAF_TO_PARENT_FORBIDDEN_PROGRESS = new ErrorCode(1_008_004_012, "拆子任务前请先将父任务进度清零");
|
||||
ErrorCode PROJECT_TASK_LEAF_TO_PARENT_FORBIDDEN_WORKLOG = new ErrorCode(1_008_004_013, "拆子任务前请先删除父任务下已填的工时记录");
|
||||
|
||||
// ========== 任务协办人 1_008_005_xxx ==========
|
||||
ErrorCode PROJECT_TASK_ASSIGNEE_NOT_EXISTS = new ErrorCode(1_008_005_001, "任务协办人记录不存在");
|
||||
ErrorCode PROJECT_TASK_ASSIGNEE_INVALID_MEMBER = new ErrorCode(1_008_005_002, "任务协办人必须是当前有效的执行成员");
|
||||
ErrorCode PROJECT_TASK_ASSIGNEE_OWNER_CONFLICT = new ErrorCode(1_008_005_003, "任务协办人不能与任务负责人重复");
|
||||
ErrorCode PROJECT_TASK_ASSIGNEE_ALREADY_ACTIVE = new ErrorCode(1_008_005_004, "该用户已是当前任务的活跃协办人");
|
||||
ErrorCode PROJECT_TASK_ASSIGNEE_NOT_ACTIVE = new ErrorCode(1_008_005_005, "任务协办人记录已失效,无需重复操作");
|
||||
ErrorCode PROJECT_TASK_ASSIGNEE_REASON_REQUIRED = new ErrorCode(1_008_005_006, "退出协办必须填写原因");
|
||||
|
||||
// ========== 任务工时 1_008_006_xxx ==========
|
||||
ErrorCode PROJECT_TASK_WORKLOG_NOT_EXISTS = new ErrorCode(1_008_006_001, "任务工时记录不存在");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_NOT_LEAF_TASK = new ErrorCode(1_008_006_002, "父任务不允许填报工时,请到具体子任务填报");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_DURATION_INVALID = new ErrorCode(1_008_006_003, "工时小时数必须大于 0 且为 0.5 的整数倍");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_NOT_OWNER_OR_ASSIGNEE = new ErrorCode(1_008_006_004, "仅任务负责人或在岗协办人可填报工时");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_EDIT_NOT_OWN = new ErrorCode(1_008_006_005, "只能修改自己填报的工时记录");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN = new ErrorCode(1_008_006_006, "仅记录填报人或任务负责人可删除该工时记录");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_DATE_RANGE_INVALID = new ErrorCode(1_008_006_007, "段起始日期不能晚于段结束日期");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_DATE_OVERLAP = new ErrorCode(1_008_006_008, "日期范围与该任务下您已有的工时记录重叠");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC = new ErrorCode(1_008_006_010, "工时进度与日期顺序不一致:早段进度不得高于晚段、晚段进度不得低于早段");
|
||||
ErrorCode PROJECT_TASK_WORKLOG_DIFFICULTY_INVALID = new ErrorCode(1_008_006_011, "完成难度不在字典范围内");
|
||||
|
||||
// ========== 任务 / 工时附件 1_008_007_xxx ==========
|
||||
ErrorCode PROJECT_TASK_ATTACHMENT_TOO_MANY = new ErrorCode(1_008_007_001, "附件数量不能超过 {} 个");
|
||||
ErrorCode PROJECT_TASK_ATTACHMENT_URL_INVALID = new ErrorCode(1_008_007_002, "附件地址非法,必须为 http/https URL 且长度不超过 1024");
|
||||
ErrorCode PROJECT_TASK_ATTACHMENT_NAME_INVALID = new ErrorCode(1_008_007_003, "附件文件名不合法(必填且长度不超过 255)");
|
||||
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_NOT_ALLOWED = new ErrorCode(1_008_007_004, "附件扩展名【{}】不在允许列表内");
|
||||
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_BLOCKED = new ErrorCode(1_008_007_005, "附件类型【{}】被禁止上传");
|
||||
|
||||
// ========== 项目需求 1_008_007_xxx ==========
|
||||
ErrorCode PROJECT_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "项目需求不存在");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_007_001, "当前项目需求状态不支持动作【{}】");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_007_002, "动作【{}】必须填写原因");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_007_003, "项目需求状态已发生变化,请刷新后重试");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_007_004, "当前项目需求状态不允许编辑");
|
||||
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_005, "只有已验收的项目需求才能关闭");
|
||||
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_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_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_HAS_CHILDREN = new ErrorCode(1_008_007_013, "存在子需求,请先删除子需求");
|
||||
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_REQUIREMENTS = new ErrorCode(1_008_007_016, "模块下存在项目需求,请先删除需求");
|
||||
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_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 ==========
|
||||
ErrorCode PERSONAL_ITEM_NOT_EXISTS = new ErrorCode(1_008_008_001, "个人事项不存在");
|
||||
ErrorCode PERSONAL_ITEM_OWNER_NOT_IN_EXECUTION = new ErrorCode(1_008_008_002, "个人事项负责人必须属于当前有效执行团队成员");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_008_003, "个人事项状态定义不存在或已停用");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_008_004, "当前个人事项状态不支持动作【{}】");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_008_005, "动作【{}】必须填写原因");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_008_006, "个人事项状态已发生变化,请刷新后重试");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_008_007, "当前个人事项状态不允许编辑");
|
||||
ErrorCode PERSONAL_ITEM_NOT_ALLOW_DELETE = new ErrorCode(1_008_008_008, "仅初始态(待开始)的个人事项允许删除");
|
||||
ErrorCode PERSONAL_ITEM_WRITE_FORBIDDEN = new ErrorCode(1_008_008_009, "无权修改个人事项");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.project.enums;
|
||||
|
||||
/**
|
||||
* 项目域字典类型常量。
|
||||
*/
|
||||
public interface ProjectDictTypeConstants {
|
||||
|
||||
/**
|
||||
* 项目类型。
|
||||
*/
|
||||
String PROJECT_TYPE = "rdms_project_type";
|
||||
|
||||
/**
|
||||
* 执行类型。
|
||||
*/
|
||||
String EXECUTION_TYPE = "rdms_project_execution_type";
|
||||
|
||||
/**
|
||||
* 优先级(任务 / 执行 共用;P0=最高 ~ P3=最低)。
|
||||
*/
|
||||
String REQ_PRIORITY = "rdms_req_priority";
|
||||
|
||||
/**
|
||||
* 工时完成难度。
|
||||
*/
|
||||
String WORKLOG_DIFFICULTY = "rdms_worklog_difficulty";
|
||||
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public final class ObjectActivityConstants {
|
||||
|
||||
// ========== 审计业务类型 ==========
|
||||
public static final String PRODUCT_BIZ_TYPE = "product";
|
||||
public static final String PROJECT_BIZ_TYPE = "project";
|
||||
public static final String MEMBER_BIZ_TYPE = "rdms_user_object_role";
|
||||
|
||||
// ========== 产品对象动作 ==========
|
||||
@@ -32,6 +33,34 @@ public final class ObjectActivityConstants {
|
||||
public static final String PRODUCT_ACTION_DELETE = "delete";
|
||||
public static final String PRODUCT_ACTION_CHANGE_MANAGER = "change_manager";
|
||||
|
||||
// ========== 项目对象动作 ==========
|
||||
public static final String PROJECT_ACTION_CREATE = "create";
|
||||
public static final String PROJECT_ACTION_UPDATE = "update";
|
||||
public static final String PROJECT_ACTION_DELETE = "delete";
|
||||
public static final String PROJECT_ACTION_CHANGE_MANAGER = "change_manager";
|
||||
public static final String PROJECT_ACTION_AUTO_START = "auto_start";
|
||||
|
||||
// ========== 项目自动推进触发动作 ==========
|
||||
public static final String PROJECT_TRIGGER_CREATE_EXECUTION = "create_execution";
|
||||
public static final String PROJECT_TRIGGER_CREATE_TASK = "create_task";
|
||||
public static final String PROJECT_TRIGGER_SCHEDULE_REQUIREMENT = "schedule_requirement";
|
||||
/**
|
||||
* 项目自动开始触发动作 —— 由执行 auto_start 反向触发。
|
||||
* 与 {@link #EXECUTION_TRIGGER_AUTO_START} 同语义;项目侧用此名走 AUTO_START_TRIGGERS 白名单。
|
||||
*/
|
||||
public static final String PROJECT_TRIGGER_EXECUTION_AUTO_START = "execution_auto_start";
|
||||
|
||||
// ========== 任务自动推进触发动作 ==========
|
||||
/**
|
||||
* 任务自动开始动作编码 —— owner 或协办人填报工时时由后端自动触发的状态流转动作。
|
||||
* 对应 rdms_object_status_transition (object_type=task, action_code=auto_start,
|
||||
* from=pending → to=active)。
|
||||
* 修改 DB 该条记录的 action_code 时必须同步修改本常量;本常量被
|
||||
* ProjectTaskServiceImpl#internalAutoStartByWorklog 引用。
|
||||
* 后续若再增加"任务自动完成 / 执行自动开始"等同类语义,按 *_ACTION_AUTO_* 命名加在本分组。
|
||||
*/
|
||||
public static final String TASK_ACTION_AUTO_START = "auto_start";
|
||||
|
||||
// ========== 状态动作 ==========
|
||||
public static final String STATUS_ACTION_PAUSE = "pause";
|
||||
public static final String STATUS_ACTION_RESUME = "resume";
|
||||
@@ -42,6 +71,33 @@ public final class ObjectActivityConstants {
|
||||
public static final String MEMBER_ACTION_ADD = "add_member";
|
||||
public static final String MEMBER_ACTION_UPDATE = "update_member";
|
||||
public static final String MEMBER_ACTION_REMOVE = "remove_member";
|
||||
/**
|
||||
* 复活动作:原 INACTIVE 成员行被重新激活(status: 1 → 0),用于把"再次新增 / update 改 role 命中老 INACTIVE 行"路径
|
||||
* 跟物理"新增 / 更新"的 audit 语义区分开。createXxxMember 命中 INACTIVE 三元组复活老行时使用本动作,避免 ADD 语义误用。
|
||||
*/
|
||||
public static final String MEMBER_ACTION_REACTIVATE = "reactivate_member";
|
||||
public static final String EXECUTION_ACTION_CREATE = "create_execution_entity";
|
||||
public static final String EXECUTION_ACTION_UPDATE = "update_execution_entity";
|
||||
public static final String EXECUTION_ACTION_DELETE = "delete_execution_entity";
|
||||
public static final String EXECUTION_ACTION_CHANGE_OWNER = "change_execution_owner";
|
||||
public static final String EXECUTION_ASSIGNEE_ACTION_ADD = "add_execution_assignee";
|
||||
public static final String EXECUTION_ASSIGNEE_ACTION_REMOVE = "remove_execution_assignee";
|
||||
public static final String TASK_ACTION_CREATE = "create_task_entity";
|
||||
public static final String TASK_ACTION_UPDATE = "update_task_entity";
|
||||
public static final String TASK_ACTION_DELETE = "delete_task_entity";
|
||||
public static final String PERSONAL_ITEM_ACTION_CREATE = "create_personal_item";
|
||||
public static final String PERSONAL_ITEM_ACTION_UPDATE = "update_personal_item";
|
||||
public static final String PERSONAL_ITEM_ACTION_DELETE = "delete_personal_item";
|
||||
|
||||
// ========== 任务协办人事件类型(B 模型 - 多行周期记录) ==========
|
||||
public static final String TASK_ASSIGNEE_ACTION_JOIN = "join";
|
||||
public static final String TASK_ASSIGNEE_ACTION_INACTIVE = "inactive";
|
||||
|
||||
// ========== 执行协办人事件类型(B 模型 - 多行周期记录) ==========
|
||||
public static final String EXECUTION_ASSIGNEE_LOG_ACTION_JOIN = "join";
|
||||
public static final String EXECUTION_ASSIGNEE_LOG_ACTION_INACTIVE = "inactive";
|
||||
public static final String EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN = "owner_transfer_in";
|
||||
public static final String EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_OUT = "owner_transfer_out";
|
||||
|
||||
public static final List<String> STATUS_ACTION_TYPES = List.of(
|
||||
STATUS_ACTION_PAUSE, STATUS_ACTION_RESUME, STATUS_ACTION_ARCHIVE, STATUS_ACTION_ABANDON);
|
||||
@@ -50,7 +106,7 @@ public final class ObjectActivityConstants {
|
||||
PRODUCT_ACTION_CREATE, PRODUCT_ACTION_CHANGE_MANAGER);
|
||||
|
||||
public static final List<String> MEMBER_TIMELINE_ACTION_TYPES = List.of(
|
||||
MEMBER_ACTION_ADD, MEMBER_ACTION_UPDATE, MEMBER_ACTION_REMOVE);
|
||||
MEMBER_ACTION_ADD, MEMBER_ACTION_UPDATE, MEMBER_ACTION_REMOVE, MEMBER_ACTION_REACTIVATE);
|
||||
|
||||
private static final Set<String> STATUS_ACTION_TYPE_SET = Set.copyOf(STATUS_ACTION_TYPES);
|
||||
|
||||
@@ -67,14 +123,40 @@ public final class ObjectActivityConstants {
|
||||
case PRODUCT_ACTION_CREATE -> "创建";
|
||||
case PRODUCT_ACTION_UPDATE -> "更新";
|
||||
case PRODUCT_ACTION_DELETE -> "删除";
|
||||
case PROJECT_ACTION_AUTO_START -> "开始推进";
|
||||
case PROJECT_TRIGGER_CREATE_EXECUTION -> "创建执行";
|
||||
case PROJECT_TRIGGER_CREATE_TASK -> "创建任务";
|
||||
case PROJECT_TRIGGER_SCHEDULE_REQUIREMENT -> "项目需求排期";
|
||||
case PROJECT_TRIGGER_EXECUTION_AUTO_START -> "执行自动开始";
|
||||
case EXECUTION_ACTION_CREATE -> "创建执行";
|
||||
case EXECUTION_ACTION_UPDATE -> "更新执行";
|
||||
case EXECUTION_ACTION_DELETE -> "删除执行";
|
||||
case EXECUTION_ACTION_CHANGE_OWNER -> "变更执行负责人";
|
||||
case EXECUTION_ASSIGNEE_ACTION_ADD -> "新增执行协办人";
|
||||
case EXECUTION_ASSIGNEE_ACTION_REMOVE -> "移出执行协办人";
|
||||
case TASK_ACTION_CREATE -> "创建任务";
|
||||
case TASK_ACTION_UPDATE -> "更新任务";
|
||||
case TASK_ACTION_DELETE -> "删除任务";
|
||||
case PERSONAL_ITEM_ACTION_CREATE -> "创建个人事项";
|
||||
case PERSONAL_ITEM_ACTION_UPDATE -> "更新个人事项";
|
||||
case PERSONAL_ITEM_ACTION_DELETE -> "删除个人事项";
|
||||
case TASK_ASSIGNEE_ACTION_JOIN -> "加入";
|
||||
case TASK_ASSIGNEE_ACTION_INACTIVE -> "退出";
|
||||
case EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN -> "转入负责人";
|
||||
case EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_OUT -> "转出负责人";
|
||||
case "start" -> "开始";
|
||||
case "block" -> "阻塞";
|
||||
case "complete" -> "完成";
|
||||
case "cancel" -> "取消";
|
||||
case STATUS_ACTION_PAUSE -> "暂停";
|
||||
case STATUS_ACTION_RESUME -> "恢复";
|
||||
case STATUS_ACTION_ARCHIVE -> "归档";
|
||||
case STATUS_ACTION_ABANDON -> "废弃";
|
||||
case PRODUCT_ACTION_CHANGE_MANAGER -> "切换产品经理";
|
||||
case PRODUCT_ACTION_CHANGE_MANAGER -> "切换负责人";
|
||||
case MEMBER_ACTION_ADD -> "新增成员";
|
||||
case MEMBER_ACTION_UPDATE -> "调整成员";
|
||||
case MEMBER_ACTION_REMOVE -> "移出成员";
|
||||
case MEMBER_ACTION_REACTIVATE -> "重新激活成员";
|
||||
default -> normalizedActionType;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
|
||||
|
||||
/**
|
||||
* RDMS 对象角色与成员关系常量。
|
||||
*/
|
||||
public final class ObjectRoleConstants {
|
||||
|
||||
private ObjectRoleConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象级权限作用域,对应 sys_menu.scope_type = object。
|
||||
*/
|
||||
public static final String ROLE_SCOPE_OBJECT = PermissionScopeTypeEnum.OBJECT.getScopeType();
|
||||
|
||||
/**
|
||||
* 对象成员有效状态,对应 rdms_user_object_role.status = 0。
|
||||
*/
|
||||
public static final Integer MEMBER_STATUS_ACTIVE = 0;
|
||||
|
||||
/**
|
||||
* 对象成员失效状态,对应 rdms_user_object_role.status = 1。
|
||||
*/
|
||||
public static final Integer MEMBER_STATUS_INACTIVE = 1;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
/**
|
||||
* 个人事项常量。
|
||||
*/
|
||||
public final class PersonalItemConstants {
|
||||
|
||||
private PersonalItemConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人事项状态模型复用任务对象类型。
|
||||
*/
|
||||
public static final String STATUS_OBJECT_TYPE = ProjectTaskConstants.OBJECT_TYPE;
|
||||
|
||||
/**
|
||||
* 个人事项业务类型。
|
||||
*/
|
||||
public static final String BIZ_TYPE = "personal_item";
|
||||
|
||||
public static final String PERMISSION_QUERY = "project:personal-item:query";
|
||||
public static final String PERMISSION_CREATE = "project:personal-item:create";
|
||||
public static final String PERMISSION_UPDATE = "project:personal-item:update";
|
||||
public static final String PERMISSION_DELETE = "project:personal-item:delete";
|
||||
public static final String PERMISSION_STATUS = "project:personal-item:status";
|
||||
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
/**
|
||||
* 产品对象常量。
|
||||
*/
|
||||
public final class ProductObjectConstants {
|
||||
|
||||
private ProductObjectConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品对象类型,对应 rdms_user_object_role.object_type、rdms_object_status_model.object_type。
|
||||
*/
|
||||
public static final String OBJECT_TYPE = "product";
|
||||
|
||||
/**
|
||||
* 产品经理对象角色编码。该角色用于初始化与识别产品负责人。
|
||||
*/
|
||||
public static final String MANAGER_ROLE_CODE = "product_manager";
|
||||
|
||||
/**
|
||||
* 产品暂停状态。
|
||||
*/
|
||||
public static final String STATUS_PAUSED = "paused";
|
||||
|
||||
/**
|
||||
* 产品自动编码前缀。
|
||||
*/
|
||||
public static final String CODE_PREFIX = "CNPD";
|
||||
|
||||
/**
|
||||
* 产品编辑权限码。
|
||||
*/
|
||||
public static final String PERMISSION_UPDATE = "project:product:update";
|
||||
|
||||
/**
|
||||
* 产品状态动作权限码。
|
||||
*/
|
||||
public static final String PERMISSION_STATUS = "project:product:status";
|
||||
|
||||
/**
|
||||
* 产品需求评审权限码。
|
||||
*/
|
||||
public static final String PERMISSION_REVIEW = "project:product:review";
|
||||
|
||||
/**
|
||||
* 产品删除权限码。
|
||||
*/
|
||||
public static final String PERMISSION_DELETE = "project:product:delete";
|
||||
|
||||
/**
|
||||
* 产品删除确认口令。
|
||||
*/
|
||||
public static final String DELETE_CONFIRM_TEXT = "DELETE";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 执行对象常量。
|
||||
*/
|
||||
public final class ProjectExecutionConstants {
|
||||
|
||||
private ProjectExecutionConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行对象类型,对应 rdms_object_status_model.object_type。
|
||||
*/
|
||||
public static final String OBJECT_TYPE = "execution";
|
||||
|
||||
/**
|
||||
* 执行业务类型。
|
||||
*/
|
||||
public static final String BIZ_TYPE = "project_execution";
|
||||
|
||||
/**
|
||||
* 创建执行权限码。
|
||||
*/
|
||||
public static final String PERMISSION_CREATE = "project:execution:create";
|
||||
|
||||
/**
|
||||
* 编辑执行权限码。
|
||||
*/
|
||||
public static final String PERMISSION_UPDATE = "project:execution:update";
|
||||
|
||||
/**
|
||||
* 执行负责人治理权限码。
|
||||
*/
|
||||
public static final String PERMISSION_OWNER = "project:execution:owner";
|
||||
|
||||
/**
|
||||
* 执行协办人治理权限码。
|
||||
*/
|
||||
public static final String PERMISSION_ASSIGNEE = "project:execution:assignee";
|
||||
|
||||
/**
|
||||
* 执行状态动作权限码。
|
||||
*/
|
||||
public static final String PERMISSION_STATUS = "project:execution:status";
|
||||
|
||||
/**
|
||||
* 删除执行权限码。
|
||||
* 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。
|
||||
*/
|
||||
public static final String PERMISSION_DELETE = "project:execution:delete";
|
||||
|
||||
/**
|
||||
* 执行"已完成"状态码,对应 rdms_object_status_model 中 object_type='execution' 且 status_code='completed' 的状态。
|
||||
* 删除时拒绝主动删除(已完成的执行不允许删除)。
|
||||
*/
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
|
||||
/**
|
||||
* 删除确认口令合法值集合;兼容大写英文 "DELETE" 与中文 "删除",前端可纯中文文案。
|
||||
* 校验时精确匹配(trim 后比对)。
|
||||
*/
|
||||
public static final Set<String> DELETE_CONFIRM_TEXTS = Set.of("DELETE", "删除");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 项目对象常量。
|
||||
*/
|
||||
public final class ProjectObjectConstants {
|
||||
|
||||
private ProjectObjectConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目对象类型,对应 rdms_user_object_role.object_type、rdms_object_status_model.object_type。
|
||||
*/
|
||||
public static final String OBJECT_TYPE = "project";
|
||||
|
||||
/**
|
||||
* 项目经理对象角色编码。该角色用于初始化与识别项目负责人。
|
||||
*/
|
||||
public static final String MANAGER_ROLE_CODE = "project_manager";
|
||||
|
||||
/**
|
||||
* 项目游客对象角色编码。创建人未成为项目成员时,用于返回只读上下文菜单。
|
||||
*/
|
||||
public static final String VISITOR_ROLE_CODE = "visitor";
|
||||
|
||||
/**
|
||||
* 产品主线项目类型字典值。后续字典值收敛后,只需调整这里。
|
||||
*/
|
||||
public static final Set<String> MAINLINE_PROJECT_TYPE_CODES = Set.of("mainline", "main", "product_mainline", "主线");
|
||||
|
||||
/**
|
||||
* 已作废项目状态编码。当前作废项目不占用产品主线项目名额。
|
||||
*/
|
||||
public static final String STATUS_CANCELLED = "cancelled";
|
||||
|
||||
/**
|
||||
* 项目自动编码前缀。
|
||||
*/
|
||||
public static final String CODE_PREFIX = "CNPJ";
|
||||
|
||||
/**
|
||||
* 项目编辑权限码。
|
||||
*/
|
||||
public static final String PERMISSION_UPDATE = "project:project:update";
|
||||
|
||||
/**
|
||||
* 项目成员维护权限码。
|
||||
*/
|
||||
public static final String PERMISSION_MEMBER = "project:project:member";
|
||||
|
||||
/**
|
||||
* 项目状态动作权限码。
|
||||
*/
|
||||
public static final String PERMISSION_STATUS = "project:project:status";
|
||||
|
||||
/**
|
||||
* 项目需求评审权限码。
|
||||
*/
|
||||
public static final String PERMISSION_REVIEW = "project:project:review";
|
||||
|
||||
/**
|
||||
* 项目拆分权限码。
|
||||
*/
|
||||
public static final String PERMISSION_SPLIT = "project:project:split";
|
||||
|
||||
/**
|
||||
* 项目删除权限码。
|
||||
*/
|
||||
public static final String PERMISSION_DELETE = "project:project:delete";
|
||||
|
||||
/**
|
||||
* 项目删除确认口令。
|
||||
*/
|
||||
public static final String DELETE_CONFIRM_TEXT = "DELETE";
|
||||
|
||||
/**
|
||||
* 可触发项目从待开始自动进入进行中的后端业务动作。
|
||||
*/
|
||||
public static final Set<String> AUTO_START_TRIGGERS = Set.of(
|
||||
ObjectActivityConstants.PROJECT_TRIGGER_CREATE_EXECUTION,
|
||||
ObjectActivityConstants.PROJECT_TRIGGER_CREATE_TASK,
|
||||
ObjectActivityConstants.PROJECT_TRIGGER_SCHEDULE_REQUIREMENT,
|
||||
ObjectActivityConstants.PROJECT_TRIGGER_EXECUTION_AUTO_START);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 任务对象常量。
|
||||
*/
|
||||
public final class ProjectTaskConstants {
|
||||
|
||||
private ProjectTaskConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务对象类型,对应 rdms_object_status_model.object_type。
|
||||
*/
|
||||
public static final String OBJECT_TYPE = "task";
|
||||
|
||||
/**
|
||||
* 任务"已完成"状态码,对应 rdms_object_status_model 中 object_type='task' 且 status_code='completed' 的状态。
|
||||
* 用于 execution 的 complete 按钮可见性判定:要求根任务在排除排除集后全部为该状态。
|
||||
*/
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
|
||||
/**
|
||||
* 任务业务类型。
|
||||
*/
|
||||
public static final String BIZ_TYPE = "project_task";
|
||||
|
||||
/**
|
||||
* 创建任务权限码。
|
||||
*/
|
||||
public static final String PERMISSION_CREATE = "project:task:create";
|
||||
|
||||
/**
|
||||
* 编辑任务权限码。
|
||||
*/
|
||||
public static final String PERMISSION_UPDATE = "project:task:update";
|
||||
|
||||
/**
|
||||
* 任务状态动作权限码。
|
||||
*/
|
||||
public static final String PERMISSION_STATUS = "project:task:status";
|
||||
|
||||
/**
|
||||
* 任务协办人维护权限码。
|
||||
*/
|
||||
public static final String PERMISSION_ASSIGNEE = "project:task:assignee";
|
||||
|
||||
/**
|
||||
* 任务工时维护权限码。
|
||||
*/
|
||||
public static final String PERMISSION_WORKLOG = "project:task:worklog";
|
||||
|
||||
/**
|
||||
* 删除任务权限码。
|
||||
* 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。
|
||||
* 实际拦截:service 内按 task.parentTaskId 分流走 checkOwnerOrProjectPermission
|
||||
* (一级任务 → execution.ownerId 字段身份;子任务 → parentTask.ownerId 字段身份;不命中字段时回落本权限码)。
|
||||
*/
|
||||
public static final String PERMISSION_DELETE = "project:task:delete";
|
||||
|
||||
/**
|
||||
* 项目任务"查看全部"权限码(对象域,object_type='project')。
|
||||
*
|
||||
* 用于跨执行视角下"项目全部任务"语义的接口/视图(page / status-board /
|
||||
* board-page / summary 的 scope=all 分支)。无此权限码时,只能看到自己作为
|
||||
* owner 或活跃协办的任务(走 VisibilityScopeResolver.resolveForProject 过滤)。
|
||||
*
|
||||
* 种子绑定:默认绑给项目负责人 + 项目创建人;普通成员、执行负责人默认不绑。
|
||||
* 实际项目中由运维在角色管理界面调整。
|
||||
*/
|
||||
public static final String PERMISSION_LIST_ALL = "project:task:list-all";
|
||||
|
||||
/**
|
||||
* 删除确认口令合法值集合;兼容大写英文 "DELETE" 与中文 "删除",前端可纯中文文案。
|
||||
* 校验时精确匹配(trim 后比对)。
|
||||
*/
|
||||
public static final Set<String> DELETE_CONFIRM_TEXTS = Set.of("DELETE", "删除");
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.constant.PersonalItemConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogSaveReqVO;
|
||||
import com.njcn.rdms.module.project.service.personal.PersonalItemService;
|
||||
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.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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 个人事项")
|
||||
@RestController
|
||||
@RequestMapping("/project/personal-items")
|
||||
@Validated
|
||||
public class PersonalItemController {
|
||||
|
||||
@Resource
|
||||
private PersonalItemService personalItemService;
|
||||
@Resource
|
||||
private ProjectExecutionService projectExecutionService;
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建个人事项")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_CREATE + "')")
|
||||
public CommonResult<Long> create(@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
||||
return success(personalItemService.createItem(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新个人事项")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> update(@PathVariable("id") Long id,
|
||||
@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
||||
personalItemService.updateItem(id, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取个人事项详情")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||
public CommonResult<PersonalItemRespVO> get(@PathVariable("id") Long id) {
|
||||
return success(personalItemService.getItemRespVO(id));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取个人事项分页")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||
public CommonResult<PageResult<PersonalItemRespVO>> page(@Valid PersonalItemPageReqVO reqVO) {
|
||||
return success(personalItemService.getItemRespVOPage(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/change-status")
|
||||
@Operation(summary = "变更个人事项状态")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_STATUS + "')")
|
||||
public CommonResult<Boolean> changeStatus(@PathVariable("id") Long id,
|
||||
@Valid @RequestBody PersonalItemStatusActionReqVO reqVO) {
|
||||
personalItemService.changeStatus(id, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/worklogs")
|
||||
@Operation(summary = "新增个人事项工作日志")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Long> createWorklog(@PathVariable("id") Long id,
|
||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||
return success(personalItemService.createWorklog(id, reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/worklogs")
|
||||
@Operation(summary = "获取个人事项工作日志分页")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||
public CommonResult<PageResult<TaskWorklogRespVO>> getWorklogPage(@PathVariable("id") Long id,
|
||||
@Valid TaskWorklogPageReqVO reqVO) {
|
||||
return success(personalItemService.getWorklogPage(id, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/worklogs/{worklogId}")
|
||||
@Operation(summary = "修改个人事项工作日志")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> updateWorklog(@PathVariable("id") Long id,
|
||||
@PathVariable("worklogId") Long worklogId,
|
||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||
personalItemService.updateWorklog(id, worklogId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/worklogs/{worklogId}")
|
||||
@Operation(summary = "删除个人事项工作日志")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> deleteWorklog(@PathVariable("id") Long id,
|
||||
@PathVariable("worklogId") Long worklogId) {
|
||||
personalItemService.deleteWorklog(id, worklogId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/worklogs/delete-list")
|
||||
@Operation(summary = "批量删除个人事项工作日志")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> deleteWorklogs(@PathVariable("id") Long id,
|
||||
@RequestParam("ids") List<Long> ids) {
|
||||
personalItemService.deleteWorklogs(id, ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除个人事项")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
||||
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
|
||||
personalItemService.deleteItem(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Operation(summary = "批量删除个人事项")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
||||
public CommonResult<Boolean> deleteList(@RequestParam("ids") List<Long> ids) {
|
||||
personalItemService.deleteItems(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/relate-execution")
|
||||
@Operation(summary = "批量个人事项关联执行")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> relateExecution(@RequestParam("itemIds") List<Long> itemIds,
|
||||
@RequestParam("executionId") Long executionId) {
|
||||
personalItemService.relateExecution(itemIds, executionId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/owner/all-execution")
|
||||
@Operation(summary = "获取当前登录用户负责的所有执行")
|
||||
public CommonResult<List<ProjectExecutionRespVO>> getCurrentUserExecutionList() {
|
||||
return success(projectExecutionService.getCurrentUserExecutionList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项生命周期动作 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class PersonalItemLifecycleActionRespVO {
|
||||
|
||||
@Schema(description = "动作编码", example = "complete")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "完成")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "是否必须填写原因", example = "true")
|
||||
private Boolean needReason;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
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 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 PersonalItemPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "关键词,匹配个人事项标题", example = "沟通纪要")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "负责人用户编号;当前阶段仅支持查询本人事项", example = "3001")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "个人事项状态编码", example = "pending")
|
||||
@Size(max = 32, message = "个人事项状态编码长度不能超过32个字符")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项 Response VO")
|
||||
@Data
|
||||
public class PersonalItemRespVO {
|
||||
|
||||
@Schema(description = "个人事项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "个人事项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "整理供应商沟通纪要")
|
||||
private String taskTitle;
|
||||
|
||||
@Schema(description = "个人事项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "todo")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "负责人用户昵称", example = "小王")
|
||||
private String ownerNickname;
|
||||
|
||||
@Schema(description = "个人事项状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "个人事项状态名称", example = "待开始")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
@Schema(description = "当前状态是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
|
||||
@Schema(description = "当前状态可执行动作")
|
||||
private List<PersonalItemLifecycleActionRespVO> availableActions;
|
||||
|
||||
@Schema(description = "个人事项进度", example = "60.00")
|
||||
private BigDecimal progressRate;
|
||||
|
||||
@Schema(description = "已填报工时合计(小时,0.5 颗粒);逻辑删除的工时记录不计入。无记录默认为 0",
|
||||
example = "8.0")
|
||||
private BigDecimal totalSpentHours;
|
||||
|
||||
@Schema(description = "计划开始日期")
|
||||
private LocalDate plannedStartDate;
|
||||
|
||||
@Schema(description = "计划结束日期")
|
||||
private LocalDate plannedEndDate;
|
||||
|
||||
@Schema(description = "实际开始日期")
|
||||
private LocalDate actualStartDate;
|
||||
|
||||
@Schema(description = "实际结束日期")
|
||||
private LocalDate actualEndDate;
|
||||
|
||||
@Schema(description = "个人事项说明")
|
||||
private String taskDesc;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "附件列表")
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项保存 Request VO")
|
||||
@Data
|
||||
public class PersonalItemSaveReqVO {
|
||||
|
||||
@Schema(description = "执行编号,仅用于创建/编辑时补充执行成员合法性校验", example = "5001")
|
||||
private Long executionId;
|
||||
|
||||
@Schema(description = "个人事项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "整理供应商沟通纪要")
|
||||
@NotBlank(message = "个人事项标题不能为空")
|
||||
@Size(max = 300, message = "个人事项标题长度不能超过300个字符")
|
||||
private String taskTitle;
|
||||
|
||||
@Schema(description = "个人事项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "todo")
|
||||
@NotBlank(message = "个人事项类型不能为空")
|
||||
@Size(max = 32, message = "个人事项类型长度不能超过32个字符")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "个人事项进度(0~100)", example = "60.00")
|
||||
@DecimalMin(value = "0.00", message = "个人事项进度不能小于 0")
|
||||
@DecimalMax(value = "100.00", message = "个人事项进度不能大于 100")
|
||||
private BigDecimal progressRate;
|
||||
|
||||
@Schema(description = "计划开始日期", example = "2026-05-20")
|
||||
private LocalDate plannedStartDate;
|
||||
|
||||
@Schema(description = "计划结束日期", example = "2026-05-25")
|
||||
private LocalDate plannedEndDate;
|
||||
|
||||
@Schema(description = "个人事项说明(富文本 HTML)")
|
||||
private String taskDesc;
|
||||
|
||||
@Schema(description = "附件列表")
|
||||
@Valid
|
||||
private List<AttachmentItem> attachments;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项状态动作 Request VO")
|
||||
@Data
|
||||
public class PersonalItemStatusActionReqVO {
|
||||
|
||||
@Schema(description = "动作编码,如 start、complete、reopen", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "complete")
|
||||
@NotBlank(message = "动作编码不能为空")
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作原因;是否必填由状态流转配置决定", example = "事项完成")
|
||||
@Size(max = 500, message = "动作原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductCreateWithTeamReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
|
||||
@@ -38,6 +40,13 @@ public class ProductController {
|
||||
return success(productService.createProduct(createReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/create-with-team")
|
||||
@Operation(summary = "创建产品并初始化团队(原子接口)")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:create')")
|
||||
public CommonResult<Long> createProductWithTeam(@Valid @RequestBody ProductCreateWithTeamReqVO reqVO) {
|
||||
return success(productService.createProductWithTeam(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新产品")
|
||||
public CommonResult<Boolean> updateProduct(@Valid @RequestBody ProductSaveReqVO updateReqVO) {
|
||||
@@ -62,12 +71,17 @@ public class ProductController {
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取产品分页")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:query')")
|
||||
public CommonResult<PageResult<ProductRespVO>> getProductPage(@Valid ProductPageReqVO pageReqVO) {
|
||||
PageResult<ProductDO> pageResult = productService.getProductPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, ProductRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/overview-summary")
|
||||
@Operation(summary = "获取产品入口页概览统计")
|
||||
public CommonResult<ProductOverviewSummaryRespVO> getProductOverviewSummary() {
|
||||
return success(productService.getProductOverviewSummary());
|
||||
}
|
||||
|
||||
@PostMapping("/change-status")
|
||||
@Operation(summary = "变更产品状态")
|
||||
public CommonResult<Boolean> changeProductStatus(@Valid @RequestBody ProductStatusActionReqVO reqVO) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO;
|
||||
@@ -42,6 +44,14 @@ public class ProductMemberController {
|
||||
return success(productMemberService.createProductMember(productId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/batch")
|
||||
@Operation(summary = "批量新增产品团队成员")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<List<Long>> batchCreateProductMembers(@PathVariable("id") Long productId,
|
||||
@Valid @RequestBody ProductMemberBatchCreateReqVO reqVO) {
|
||||
return success(productMemberService.batchCreateProductMembers(productId, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/members/{memberId}")
|
||||
@Operation(summary = "调整产品团队成员角色")
|
||||
public CommonResult<Boolean> updateProductMember(@PathVariable("id") Long productId,
|
||||
@@ -60,4 +70,13 @@ public class ProductMemberController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/batch/inactive")
|
||||
@Operation(summary = "批量移出产品团队成员")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> batchInactiveProductMembers(@PathVariable("id") Long productId,
|
||||
@Valid @RequestBody ProductMemberBatchInactiveReqVO reqVO) {
|
||||
productMemberService.batchInactiveProductMembers(productId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ public class ProductRequirementController {
|
||||
@Resource
|
||||
private ProductRequirementService requirementService;
|
||||
|
||||
// ========== 需求管理 ==========
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建产品需求")
|
||||
public CommonResult<Long> createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) {
|
||||
@@ -59,11 +57,18 @@ public class ProductRequirementController {
|
||||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
@Operation(summary = "获取需求树形列表(分页)")
|
||||
@Operation(summary = "获取需求树分页列表")
|
||||
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementTree(@Valid ProductRequirementPageReqVO 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")
|
||||
@Operation(summary = "变更需求状态")
|
||||
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) {
|
||||
@@ -81,8 +86,6 @@ public class ProductRequirementController {
|
||||
@PostMapping("/split")
|
||||
@Operation(summary = "拆分产品需求")
|
||||
public CommonResult<Long> splitRequirement(@Valid @RequestBody ProductRequirementSplitReqVO reqVO) {
|
||||
System.out.println("-----------------------");
|
||||
System.out.println(reqVO);
|
||||
return success(requirementService.splitRequirement(reqVO));
|
||||
}
|
||||
|
||||
@@ -103,17 +106,37 @@ public class ProductRequirementController {
|
||||
return success(requirementService.getAllowedTransitions(requirementId, productId));
|
||||
}
|
||||
|
||||
@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));
|
||||
@PostMapping("/allowed-transitions/batch")
|
||||
@Operation(summary = "批量获取需求可执行的状态动作列表")
|
||||
public CommonResult<List<ProductRequirementAllowedTransitionBatchRespVO>> getAllowedTransitionsBatch(
|
||||
@Valid @RequestBody ProductRequirementBatchReqVO reqVO) {
|
||||
return success(requirementService.getAllowedTransitionsBatch(reqVO));
|
||||
}
|
||||
|
||||
// ========== 模块管理 ==========
|
||||
@GetMapping("/has-dispatched")
|
||||
@Operation(summary = "判断产品需求是否已分流生成项目需求")
|
||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> hasDispatchedProjectRequirement(
|
||||
@RequestParam("requirementId") Long requirementId,
|
||||
@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.hasDispatchedProjectRequirement(requirementId, productId));
|
||||
}
|
||||
|
||||
@PostMapping("/has-dispatched/batch")
|
||||
@Operation(summary = "批量判断产品需求是否已分流生成项目需求")
|
||||
public CommonResult<List<ProductRequirementHasDispatchedBatchRespVO>> hasDispatchedProjectRequirementBatch(
|
||||
@Valid @RequestBody ProductRequirementBatchReqVO reqVO) {
|
||||
return success(requirementService.hasDispatchedProjectRequirementBatch(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/dispatched-project-link")
|
||||
@Operation(summary = "获取产品需求分流后对应的项目需求跳转链接")
|
||||
@Parameter(name = "productRequirementId", description = "产品需求编号", required = true, example = "1024")
|
||||
public CommonResult<ProductRequirementDispatchedProjectLinkRespVO> getDispatchedProjectLink(
|
||||
@RequestParam("productRequirementId") Long productRequirementId) {
|
||||
return success(requirementService.getDispatchedProjectLink(productRequirementId));
|
||||
}
|
||||
|
||||
@PostMapping("/module/create")
|
||||
@Operation(summary = "创建需求模块")
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 产品团队成员批量新增 Request VO")
|
||||
@Data
|
||||
public class ProductMemberBatchCreateReqVO {
|
||||
|
||||
/**
|
||||
* 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。
|
||||
* 经理角色(product_manager)由 Service 兜底拦截,不在此体现。
|
||||
*/
|
||||
@Schema(description = "待新增的成员列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "请至少选择一名成员")
|
||||
@Size(max = 200, message = "单次批量加入成员不能超过 200 人")
|
||||
@Valid
|
||||
private List<Item> members;
|
||||
|
||||
@Schema(description = "批量新增成员项")
|
||||
@Data
|
||||
public static class Item {
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002001")
|
||||
@NotNull(message = "角色编号不能为空")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "备注", example = "本次批量加入")
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 产品团队成员批量移出 Request VO")
|
||||
@Data
|
||||
public class ProductMemberBatchInactiveReqVO {
|
||||
|
||||
/**
|
||||
* 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。
|
||||
* 经理、已失效、重复 memberId 等业务规则由 Service 兜底,不在此体现。
|
||||
*/
|
||||
@Schema(description = "待移出的成员关系 ID 列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "请至少选择一名成员")
|
||||
@Size(max = 200, message = "单次批量移出成员不能超过 200 人")
|
||||
private List<Long> memberIds;
|
||||
|
||||
@Schema(description = "移出原因,单一字符串应用于本批所有成员", example = "组织架构调整,本批成员退出当前产品团队")
|
||||
@Size(max = 500, message = "移出原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 产品团队成员 Response VO")
|
||||
@Data
|
||||
@@ -42,4 +45,7 @@ public class ProductMemberRespVO {
|
||||
@Schema(description = "备注", example = "当前负责需求收敛")
|
||||
private String remark;
|
||||
|
||||
@ArraySchema(schema = @Schema(description = "非主角色的中文名列表,多角色场景使用(如同人 manager + creator);单角色时为空数组", example = "产品创建者"))
|
||||
private List<String> additionalRoleNames = Collections.emptyList();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 产品上下文中的当前角色 Response VO")
|
||||
@Data
|
||||
public class ProductContextRoleRespVO {
|
||||
@@ -10,10 +14,16 @@ public class ProductContextRoleRespVO {
|
||||
@Schema(description = "对象角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3201")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "对象角色编码", example = "product_manager")
|
||||
@Schema(description = "对象角色编码(主角色 code,权限判断兼容字段)", example = "product_manager")
|
||||
private String roleCode;
|
||||
|
||||
@Schema(description = "对象角色名称", example = "产品经理")
|
||||
@Schema(description = "对象角色名称(主角色 name)", example = "产品经理")
|
||||
private String roleName;
|
||||
|
||||
@Schema(description = "是否游客上下文(隐式 observer 兜底时为 true)", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
|
||||
private Boolean guestFlag;
|
||||
|
||||
@ArraySchema(schema = @Schema(description = "非主角色的中文名列表,多角色场景使用;单角色时为空数组", example = "创建者"))
|
||||
private List<String> additionalRoleNames = Collections.emptyList();
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 产品创建原子接口请求 VO(POST /project/product/create-with-team)。
|
||||
*
|
||||
* <p>由前端"产品两步向导"一次性提交:产品基础资料 + 初始团队成员。
|
||||
* 后端必须在同一事务内完成全部写入,任一步失败整体回滚。
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品创建(含初始团队)Request VO")
|
||||
@Data
|
||||
public class ProductCreateWithTeamReqVO {
|
||||
|
||||
/**
|
||||
* 产品基础资料。沿用 {@link ProductSaveReqVO} 字段约束(同 POST /create)。
|
||||
*/
|
||||
@Schema(description = "产品基础资料", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "产品基础资料不能为空")
|
||||
@Valid
|
||||
private ProductSaveReqVO product;
|
||||
|
||||
/**
|
||||
* 初始团队成员列表。
|
||||
*
|
||||
* <p>必须包含一条 {@code userId == product.managerUserId} 的产品经理成员,
|
||||
* 由前端在打开第 2 步时按 role code = {@code product_manager} 反查 roleId 后聚合提交。
|
||||
* 后端不再根据 {@code product.managerUserId} 自动追加经理成员。
|
||||
*/
|
||||
@Schema(description = "初始团队成员(含产品经理本人)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "初始团队成员不能为空")
|
||||
@Valid
|
||||
private List<ProductMemberSaveReqVO> members;
|
||||
|
||||
/**
|
||||
* 关心人 user ID 列表(创建时手动添加,可选)。
|
||||
*
|
||||
* <p>跟 members 是平行字段:watcher 不参与团队管理,只是被授予"产品关心人"角色(product_watcher)拿
|
||||
* 数据可见性。允许跟 members 的 user 重叠(多角色合法);后端按 (user, object, role) 三元组写入,
|
||||
* 重复跳过 / INACTIVE 复活,业务侧不强校验。
|
||||
*/
|
||||
@Schema(description = "关心人用户ID列表(可选,可与团队成员重叠)", example = "101,102")
|
||||
private List<Long> watcherUserIds;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "管理后台 - 产品入口页概览统计 Response VO")
|
||||
@Data
|
||||
public class ProductOverviewSummaryRespVO {
|
||||
|
||||
@Schema(description = "产品状态数量,按当前启用的产品状态模型返回")
|
||||
private Map<String, Long> statusCounts;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
|
||||
@Schema(description = "管理后台 - 产品需求批量可执行动作 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementAllowedTransitionBatchRespVO {
|
||||
|
||||
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long requirementId;
|
||||
|
||||
@Schema(description = "可执行动作列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<ProductRequirementStatusTransitionRespVO> transitions;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 产品需求批量查询 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementBatchReqVO {
|
||||
|
||||
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "需求编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]")
|
||||
@NotEmpty(message = "需求编号列表不能为空")
|
||||
private List<Long> requirementIds;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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 ProductRequirementDispatchedProjectLinkRespVO {
|
||||
|
||||
@Schema(description = "项目需求ID", example = "10086")
|
||||
private Long projectRequirementId;
|
||||
|
||||
@Schema(description = "关联项目ID", example = "8888")
|
||||
private Long projectId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品需求批量指派状态 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementHasDispatchedBatchRespVO {
|
||||
|
||||
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long requirementId;
|
||||
|
||||
@Schema(description = "是否已指派生成项目需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
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,8 +1,11 @@
|
||||
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 io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@@ -16,19 +19,19 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "父需求ID(0表示顶级需求)", example = "0")
|
||||
@Schema(description = "父需求ID,0 表示顶级需求", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "所属模块ID", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", example = "0")
|
||||
@Schema(description = "是否需要评审,0不需要,1需要", example = "0")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@@ -37,13 +40,13 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "需求分类名称", example = "功能需求")
|
||||
private String categoryName;
|
||||
|
||||
@Schema(description = "来源类型(manual:手工新增, work_order:工单流转)", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual")
|
||||
@Schema(description = "需求来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "来源业务ID", example = "1024")
|
||||
private Long sourceBizId;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@Schema(description = "优先级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "优先级名称", example = "中")
|
||||
@@ -52,19 +55,22 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "当前状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending_dispatch")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "当前状态名称", example = "待分流")
|
||||
@Schema(description = "当前状态名称", example = "待指派")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
||||
@Schema(description = "最近一次状态动作原因", example = "需求全部结束")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "提出人用户姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
@Schema(description = "预期完成时间", example = "2026-05-31")
|
||||
private LocalDate expectedTime;
|
||||
|
||||
@Schema(description = "当前处理人用户ID", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
@@ -76,9 +82,6 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "实现项目名称", example = "NPQS-10086")
|
||||
private String implementProjectName;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@@ -88,10 +91,9 @@ public class ProductRequirementRespVO {
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "子需求列表(树形结构)")
|
||||
@Schema(description = "附件列表")
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
@Schema(description = "子需求列表,树形结构")
|
||||
private List<ProductRequirementRespVO> children;
|
||||
|
||||
@Schema(description = "是否为终态(已拒绝、已取消、已关闭)", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求保存 Request VO
|
||||
@@ -22,10 +25,10 @@ public class ProductRequirementSaveReqVO {
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
@Schema(description = "所属模块ID,为空时归入全部需求模块", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@Schema(description = "是否需要评审,0不需要,1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@@ -34,7 +37,7 @@ public class ProductRequirementSaveReqVO {
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@@ -42,25 +45,34 @@ public class ProductRequirementSaveReqVO {
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@Schema(description = "优先级,0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
@Schema(description = "提出人姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "预期完成时间", example = "2026-05-31")
|
||||
private LocalDate expectedTime;
|
||||
|
||||
@Schema(description = "当前处理人用户ID", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
private String currentHandlerUserNickname;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
@Schema(description = "排序值,越小越靠前", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "附件列表")
|
||||
@Valid
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求拆分 Request VO
|
||||
@@ -23,10 +26,10 @@ public class ProductRequirementSplitReqVO {
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
@Schema(description = "所属模块ID,为空时归入全部需求模块", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@Schema(description = "是否需要评审,0不需要,1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@@ -35,7 +38,7 @@ public class ProductRequirementSplitReqVO {
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@@ -43,25 +46,34 @@ public class ProductRequirementSplitReqVO {
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@Schema(description = "优先级,0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
@Schema(description = "提出人姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "预期完成时间", example = "2026-05-31")
|
||||
private LocalDate expectedTime;
|
||||
|
||||
@Schema(description = "当前处理人用户ID", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
private String currentHandlerUserNickname;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
@Schema(description = "排序值,越小越靠前", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "附件列表")
|
||||
@Valid
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ public class ProductRequirementStatusActionReqVO {
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "状态变更原因", example = "评审通过,进入分流阶段")
|
||||
@Schema(description = "状态变更原因", example = "需求全部完成")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "实现项目编号(dispatch动作时可选)", example = "1024")
|
||||
@Schema(description = "关联项目编号(dispatch动作时可选)", example = "1024")
|
||||
private Long implementProjectId;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import lombok.Data;
|
||||
@Data
|
||||
public class ProductRequirementStatusDictRespVO {
|
||||
|
||||
@Schema(description = "状态编码", example = "pending_confirm")
|
||||
@Schema(description = "状态编码", example = "pending_claim")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "状态名称", example = "待确认")
|
||||
@Schema(description = "状态名称", example = "待认领")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "排序值", example = "1")
|
||||
@@ -25,4 +25,7 @@ public class ProductRequirementStatusDictRespVO {
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminalFlag;
|
||||
|
||||
@Schema(description = "是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ProductRequirementStatusTransitionRespVO {
|
||||
@Schema(description = "动作编码", example = "dispatch")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "明确分流/拆分")
|
||||
@Schema(description = "动作名称", example = "明确指派/拆分")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "目标状态编码", example = "implementing")
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求编辑 Request VO
|
||||
@@ -23,10 +26,10 @@ public class ProductRequirementUpdateReqVO {
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
@Schema(description = "所属模块ID,为空时归入全部需求模块", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@Schema(description = "是否需要评审,0不需要,1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@@ -35,7 +38,7 @@ public class ProductRequirementUpdateReqVO {
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@@ -43,25 +46,34 @@ public class ProductRequirementUpdateReqVO {
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@Schema(description = "优先级,0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
@Schema(description = "提出人姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "预期完成时间", example = "2026-05-31")
|
||||
private LocalDate expectedTime;
|
||||
|
||||
@Schema(description = "当前处理人用户ID", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
private String currentHandlerUserNickname;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
@Schema(description = "排序值,越小越靠前", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "附件列表")
|
||||
@Valid
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectContextRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectCreateWithTeamReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectOverviewSummaryRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||
import com.njcn.rdms.module.project.service.project.ProjectService;
|
||||
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.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 项目管理")
|
||||
@RestController
|
||||
@RequestMapping("/project/project")
|
||||
@Validated
|
||||
public class ProjectController {
|
||||
|
||||
@Resource
|
||||
private ProjectService projectService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建项目")
|
||||
@PreAuthorize("@ss.hasPermission('project:project:create')")
|
||||
public CommonResult<Long> createProject(@Valid @RequestBody ProjectSaveReqVO createReqVO) {
|
||||
return success(projectService.createProject(createReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/create-with-team")
|
||||
@Operation(summary = "创建项目并初始化团队(原子接口)")
|
||||
@PreAuthorize("@ss.hasPermission('project:project:create')")
|
||||
public CommonResult<Long> createProjectWithTeam(@Valid @RequestBody ProjectCreateWithTeamReqVO reqVO) {
|
||||
return success(projectService.createProjectWithTeam(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新项目")
|
||||
public CommonResult<Boolean> updateProject(@Valid @RequestBody ProjectSaveReqVO updateReqVO) {
|
||||
projectService.updateProject(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取项目详情")
|
||||
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<ProjectRespVO> getProject(@RequestParam("id") Long id) {
|
||||
return success(projectService.getProjectDetail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/context")
|
||||
@Operation(summary = "获取项目上下文")
|
||||
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<ProjectContextRespVO> getProjectContext(@PathVariable("id") Long id) {
|
||||
return success(projectService.getProjectContext(id));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取项目分页")
|
||||
public CommonResult<PageResult<ProjectRespVO>> getProjectPage(@Valid ProjectPageReqVO pageReqVO) {
|
||||
PageResult<ProjectDO> pageResult = projectService.getProjectPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, ProjectRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/list-by-product")
|
||||
@Operation(summary = "根据产品编号获取该产品下的全部项目")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<List<ProjectRespVO>> getProjectListByProductId(@RequestParam("productId") Long productId) {
|
||||
return success(projectService.getProjectListByProductId(productId));
|
||||
}
|
||||
|
||||
@GetMapping("/overview-summary")
|
||||
@Operation(summary = "获取项目入口页概览统计")
|
||||
public CommonResult<ProjectOverviewSummaryRespVO> getProjectOverviewSummary() {
|
||||
return success(projectService.getProjectOverviewSummary());
|
||||
}
|
||||
|
||||
@PostMapping("/change-status")
|
||||
@Operation(summary = "变更项目状态")
|
||||
public CommonResult<Boolean> changeProjectStatus(@Valid @RequestBody ProjectStatusActionReqVO reqVO) {
|
||||
projectService.changeProjectStatus(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除项目")
|
||||
public CommonResult<Boolean> deleteProject(@Valid @RequestBody ProjectDeleteReqVO reqVO) {
|
||||
projectService.deleteProject(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.service.project.ProjectMemberService;
|
||||
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.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 项目成员")
|
||||
@RestController
|
||||
@RequestMapping("/project/project")
|
||||
@Validated
|
||||
public class ProjectMemberController {
|
||||
|
||||
@Resource
|
||||
private ProjectMemberService projectMemberService;
|
||||
|
||||
@GetMapping("/{id}/members")
|
||||
@Operation(summary = "获取项目成员列表")
|
||||
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<List<ProjectMemberRespVO>> getProjectMemberList(@PathVariable("id") Long projectId) {
|
||||
return success(projectMemberService.getProjectMemberList(projectId));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members")
|
||||
@Operation(summary = "新增项目成员")
|
||||
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<Long> createProjectMember(@PathVariable("id") Long projectId,
|
||||
@Valid @RequestBody ProjectMemberSaveReqVO reqVO) {
|
||||
return success(projectMemberService.createProjectMember(projectId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/batch")
|
||||
@Operation(summary = "批量新增项目成员")
|
||||
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<List<Long>> batchCreateProjectMembers(@PathVariable("id") Long projectId,
|
||||
@Valid @RequestBody ProjectMemberBatchCreateReqVO reqVO) {
|
||||
return success(projectMemberService.batchCreateProjectMembers(projectId, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/members/{memberId}")
|
||||
@Operation(summary = "调整项目成员角色")
|
||||
public CommonResult<Boolean> updateProjectMember(@PathVariable("id") Long projectId,
|
||||
@PathVariable("memberId") Long memberId,
|
||||
@Valid @RequestBody ProjectMemberUpdateReqVO reqVO) {
|
||||
projectMemberService.updateProjectMember(projectId, memberId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/{memberId}/inactive")
|
||||
@Operation(summary = "移出项目成员")
|
||||
public CommonResult<Boolean> inactiveProjectMember(@PathVariable("id") Long projectId,
|
||||
@PathVariable("memberId") Long memberId,
|
||||
@Valid @RequestBody ProjectMemberInactiveReqVO reqVO) {
|
||||
projectMemberService.inactiveProjectMember(projectId, memberId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/batch/inactive")
|
||||
@Operation(summary = "批量移出项目成员")
|
||||
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> batchInactiveProjectMembers(@PathVariable("id") Long projectId,
|
||||
@Valid @RequestBody ProjectMemberBatchInactiveReqVO reqVO) {
|
||||
projectMemberService.batchInactiveProjectMembers(projectId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project;
|
||||
|
||||
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.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.ProjectRequirementCloseReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementDeleteReqVO;
|
||||
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.ProjectRequirementModuleRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSplitReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusDictRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusTransitionRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
||||
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.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - 项目需求控制器
|
||||
*/
|
||||
@Tag(name = "管理后台 - 项目需求")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/requirement")
|
||||
@Validated
|
||||
public class ProjectRequirementController {
|
||||
|
||||
@Resource
|
||||
private ProjectRequirementService requirementService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建项目需求")
|
||||
public CommonResult<Long> createRequirement(@Valid @RequestBody ProjectRequirementSaveReqVO createReqVO) {
|
||||
return success(requirementService.createRequirement(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新项目需求")
|
||||
public CommonResult<Boolean> updateRequirement(@Valid @RequestBody ProjectRequirementUpdateReqVO updateReqVO) {
|
||||
requirementService.updateRequirement(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取需求详情")
|
||||
@Parameter(name = "id", description = "需求编号", required = true, example = "1024")
|
||||
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<ProjectRequirementRespVO> getRequirement(@RequestParam("id") Long id,
|
||||
@RequestParam("projectId") Long projectId) {
|
||||
return success(requirementService.getRequirement(id, projectId));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取需求分页列表")
|
||||
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementPage(@Valid ProjectRequirementPageReqVO pageReqVO) {
|
||||
return success(requirementService.getRequirementPage(pageReqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
@Operation(summary = "获取需求树分页列表")
|
||||
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementTree(@Valid ProjectRequirementPageReqVO pageReqVO) {
|
||||
return success(requirementService.getRequirementTree(pageReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/change-status")
|
||||
@Operation(summary = "变更需求状态")
|
||||
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProjectRequirementStatusActionReqVO reqVO) {
|
||||
requirementService.changeRequirementStatus(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除项目需求")
|
||||
public CommonResult<Boolean> deleteRequirement(@Valid @RequestBody ProjectRequirementDeleteReqVO reqVO) {
|
||||
requirementService.deleteRequirement(reqVO.getId(), reqVO.getProjectId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/split")
|
||||
@Operation(summary = "拆分项目需求")
|
||||
public CommonResult<Long> splitRequirement(@Valid @RequestBody ProjectRequirementSplitReqVO reqVO) {
|
||||
return success(requirementService.splitRequirement(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/close")
|
||||
@Operation(summary = "关闭项目需求")
|
||||
public CommonResult<Boolean> closeRequirement(@Valid @RequestBody ProjectRequirementCloseReqVO reqVO) {
|
||||
requirementService.closeRequirement(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/allowed-transitions")
|
||||
@Operation(summary = "获取需求可执行的状态动作列表")
|
||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
|
||||
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<List<ProjectRequirementStatusTransitionRespVO>> getAllowedTransitions(
|
||||
@RequestParam("requirementId") Long requirementId,
|
||||
@RequestParam("projectId") Long projectId) {
|
||||
return success(requirementService.getAllowedTransitions(requirementId, projectId));
|
||||
}
|
||||
|
||||
@PostMapping("/allowed-transitions/batch")
|
||||
@Operation(summary = "批量获取需求可执行的状态动作列表")
|
||||
public CommonResult<List<ProjectRequirementAllowedTransitionBatchRespVO>> getAllowedTransitionsBatch(
|
||||
@Valid @RequestBody ProjectRequirementBatchReqVO reqVO) {
|
||||
return success(requirementService.getAllowedTransitionsBatch(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/module/create")
|
||||
@Operation(summary = "创建需求模块")
|
||||
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) {
|
||||
return success(requirementService.createRequirementModule(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/module/update")
|
||||
@Operation(summary = "更新需求模块")
|
||||
public CommonResult<Boolean> updateRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) {
|
||||
requirementService.updateRequirementModule(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/module/delete")
|
||||
@Operation(summary = "删除需求模块")
|
||||
public CommonResult<Boolean> deleteRequirementModule(@Valid @RequestBody ProjectRequirementModuleDeleteReqVO reqVO) {
|
||||
requirementService.deleteRequirementModule(reqVO.getId(), reqVO.getProjectId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/module/tree")
|
||||
@Operation(summary = "获取需求模块树")
|
||||
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<List<ProjectRequirementModuleRespVO>> getRequirementModuleTree(@RequestParam("projectId") Long projectId) {
|
||||
return success(requirementService.getRequirementModuleTree(projectId));
|
||||
}
|
||||
|
||||
@GetMapping("/status/dict")
|
||||
@Operation(summary = "获取需求所有状态字典")
|
||||
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementStatusDict() {
|
||||
return success(requirementService.getRequirementStatusDict());
|
||||
}
|
||||
|
||||
@GetMapping("/status/dict/terminal")
|
||||
@Operation(summary = "获取需求终止态状态字典")
|
||||
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
|
||||
return success(requirementService.getRequirementTerminalStatusDict());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeSaveReqVO;
|
||||
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionAssigneeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 执行协办人")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/{projectId}/executions/{executionId}")
|
||||
@Validated
|
||||
public class ProjectExecutionAssigneeController {
|
||||
|
||||
@Resource
|
||||
private ProjectExecutionAssigneeService projectExecutionAssigneeService;
|
||||
|
||||
@GetMapping("/assignees")
|
||||
@Operation(summary = "获取执行协办人列表(仅当前活跃)")
|
||||
public CommonResult<List<ExecutionAssigneeRespVO>> getExecutionAssigneeList(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId) {
|
||||
return success(projectExecutionAssigneeService.getExecutionAssigneeList(projectId, executionId));
|
||||
}
|
||||
|
||||
@PostMapping("/assignees")
|
||||
@Operation(summary = "新增执行协办人(B 模型 - 每次新增一段)")
|
||||
public CommonResult<Long> createExecutionAssignee(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid @RequestBody ExecutionAssigneeSaveReqVO reqVO) {
|
||||
return success(projectExecutionAssigneeService.createExecutionAssignee(projectId, executionId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/assignees/{assigneeId}/inactive")
|
||||
@Operation(summary = "失效执行协办人(永久保留 removedReason)")
|
||||
public CommonResult<Boolean> inactiveExecutionAssignee(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("assigneeId") Long assigneeId,
|
||||
@Valid @RequestBody ExecutionAssigneeInactiveReqVO reqVO) {
|
||||
projectExecutionAssigneeService.inactiveExecutionAssignee(projectId, executionId, assigneeId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/assignee-logs")
|
||||
@Operation(summary = "获取执行协办人变更历史(分页)")
|
||||
public CommonResult<PageResult<ExecutionAssigneeLogRespVO>> getExecutionAssigneeLogPage(
|
||||
@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid ExecutionAssigneeLogPageReqVO reqVO) {
|
||||
return success(projectExecutionAssigneeService.getExecutionAssigneeLogPage(projectId, executionId, reqVO));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
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.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.ProjectExecutionStatusBoardReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionDeletePrecheckRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.service.project.ProjectStatusBoardService;
|
||||
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService;
|
||||
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.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 项目执行")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/{projectId}/executions")
|
||||
@Validated
|
||||
public class ProjectExecutionController {
|
||||
|
||||
@Resource
|
||||
private ProjectExecutionService projectExecutionService;
|
||||
@Resource
|
||||
private ProjectStatusBoardService projectStatusBoardService;
|
||||
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建执行")
|
||||
public CommonResult<Long> createExecution(@PathVariable("projectId") Long projectId,
|
||||
@Valid @RequestBody ProjectExecutionCreateReqVO reqVO) {
|
||||
return success(projectExecutionService.createExecution(projectId, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{executionId}")
|
||||
@Operation(summary = "编辑执行")
|
||||
public CommonResult<Boolean> updateExecution(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid @RequestBody ProjectExecutionUpdateReqVO reqVO) {
|
||||
reqVO.setId(executionId);
|
||||
projectExecutionService.updateExecution(projectId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/{executionId}")
|
||||
@Operation(summary = "获取执行详情")
|
||||
public CommonResult<ProjectExecutionRespVO> getExecution(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId) {
|
||||
return success(projectExecutionService.getExecutionRespVO(projectId, executionId));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取执行分页")
|
||||
public CommonResult<PageResult<ProjectExecutionRespVO>> getExecutionPage(@PathVariable("projectId") Long projectId,
|
||||
@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));
|
||||
}
|
||||
|
||||
@GetMapping("/status-board")
|
||||
@Operation(summary = "获取执行状态看板")
|
||||
public CommonResult<ProjectExecutionStatusBoardRespVO> getExecutionStatusBoard(@PathVariable("projectId") Long projectId,
|
||||
@Valid ProjectExecutionStatusBoardReqVO reqVO) {
|
||||
return success(projectStatusBoardService.getExecutionStatusBoard(projectId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/{executionId}/change-owner")
|
||||
@Operation(summary = "变更执行负责人")
|
||||
public CommonResult<Boolean> changeOwner(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid @RequestBody ProjectExecutionOwnerChangeReqVO reqVO) {
|
||||
projectExecutionService.changeOwner(projectId, executionId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{executionId}/change-status")
|
||||
@Operation(summary = "变更执行状态")
|
||||
public CommonResult<Boolean> changeStatus(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid @RequestBody ProjectExecutionStatusActionReqVO reqVO) {
|
||||
projectExecutionService.changeExecutionStatus(projectId, executionId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/{executionId}/delete-precheck")
|
||||
@Operation(summary = "执行删除预检:返回下挂任务数 + 是否需要重型确认")
|
||||
public CommonResult<ProjectExecutionDeletePrecheckRespVO> precheckDeleteExecution(
|
||||
@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId) {
|
||||
return success(projectExecutionService.precheckDeleteExecution(projectId, executionId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{executionId}")
|
||||
@Operation(summary = "删除执行(已完成态禁止,三重确认)")
|
||||
public CommonResult<Boolean> deleteExecution(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid @RequestBody ProjectExecutionDeleteReqVO reqVO) {
|
||||
projectExecutionService.deleteExecution(projectId, executionId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 执行协办人失效 Request VO")
|
||||
@Data
|
||||
public class ExecutionAssigneeInactiveReqVO {
|
||||
|
||||
@Schema(description = "失效原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "阶段性退出")
|
||||
@NotBlank(message = "失效原因不能为空")
|
||||
@Size(max = 500, message = "失效原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 执行协办人变更历史分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ExecutionAssigneeLogPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "事件类型多选;不传表示全部",
|
||||
example = "[\"join\",\"inactive\",\"owner_transfer_in\",\"owner_transfer_out\"]")
|
||||
private List<String> actionTypes;
|
||||
|
||||
@Schema(description = "协办人用户编号;不传表示全部")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "起始时间(含),按 actionTime 比较")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Schema(description = "截止时间(含),按 actionTime 比较")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 执行协办人变更历史 Response VO")
|
||||
@Data
|
||||
public class ExecutionAssigneeLogRespVO {
|
||||
|
||||
@Schema(description = "日志编号", example = "12001")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属执行编号", example = "5001")
|
||||
private Long executionId;
|
||||
|
||||
@Schema(description = "事件类型:join / inactive / owner_transfer_in / owner_transfer_out", example = "join")
|
||||
private String actionType;
|
||||
|
||||
@Schema(description = "被操作人用户编号", example = "3002")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "被操作人昵称,返回时按当前用户信息回填")
|
||||
private String userNicknameSnapshot;
|
||||
|
||||
@Schema(description = "操作人用户编号", example = "3001")
|
||||
private Long operatorUserId;
|
||||
|
||||
@Schema(description = "操作人昵称,返回时按当前用户信息回填")
|
||||
private String operatorNicknameSnapshot;
|
||||
|
||||
@Schema(description = "事件时间")
|
||||
private LocalDateTime actionTime;
|
||||
|
||||
@Schema(description = "原因;inactive 必填,其余可空")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 执行协办人 Response VO")
|
||||
@Data
|
||||
public class ExecutionAssigneeRespVO {
|
||||
|
||||
@Schema(description = "协办人关系编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "7001")
|
||||
private Long id;
|
||||
@Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
||||
private Long executionId;
|
||||
@Schema(description = "协办人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002")
|
||||
private Long userId;
|
||||
@Schema(description = "协办人用户昵称")
|
||||
private String userNickname;
|
||||
@Schema(description = "加入时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime joinedAt;
|
||||
@Schema(description = "失效时间")
|
||||
private LocalDateTime removedAt;
|
||||
@Schema(description = "失效原因")
|
||||
private String removedReason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 执行协办人新增 Request VO")
|
||||
@Data
|
||||
public class ExecutionAssigneeSaveReqVO {
|
||||
|
||||
@Schema(description = "协办人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002")
|
||||
@NotNull(message = "协办人用户不能为空")
|
||||
private Long userId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建执行 Request VO。
|
||||
* <p>
|
||||
* 含 ownerId(必填)+ assigneeUserIds(创建时同步装配协办人)。
|
||||
* 后续编辑主数据走 PUT + {@link ProjectExecutionUpdateReqVO}(不含 ownerId / 协办人字段,避免越权裂缝)。
|
||||
*/
|
||||
@Schema(description = "管理后台 - 执行创建 Request VO")
|
||||
@Data
|
||||
public class ProjectExecutionCreateReqVO {
|
||||
|
||||
@Schema(description = "执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调")
|
||||
@NotBlank(message = "执行名称不能为空")
|
||||
@Size(max = 200, message = "执行名称长度不能超过200个字符")
|
||||
private String executionName;
|
||||
|
||||
@Schema(description = "执行类型,取值来自字典 rdms_project_execution_type", requiredMode = Schema.RequiredMode.REQUIRED, example = "feature")
|
||||
@NotBlank(message = "执行类型不能为空")
|
||||
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
||||
private String executionType;
|
||||
|
||||
@Schema(description = "优先级字典 rdms_req_priority 值,0=P0(最高) / 1=P1 / 2=P2 / 3=P3(最低)",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotBlank(message = "优先级不能为空")
|
||||
@Size(max = 8, message = "优先级长度不能超过8个字符")
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "执行负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||
@NotNull(message = "执行负责人不能为空")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "关联项目需求编号,可选;不传或传 null 表示无主执行。传入值必须属于本项目且需求非终态", example = "9001")
|
||||
private Long projectRequirementId;
|
||||
|
||||
@Schema(description = "计划开始日期")
|
||||
private LocalDate plannedStartDate;
|
||||
|
||||
@Schema(description = "计划结束日期")
|
||||
private LocalDate plannedEndDate;
|
||||
|
||||
@Schema(description = "执行说明(接受 HTML 富文本,图片走 URL 引用;后端经全局 XSS Safelist 自动净化)",
|
||||
example = "接口联调与问题跟踪")
|
||||
@Size(max = 200000, message = "执行说明长度不能超过200000个字符")
|
||||
private String executionDesc;
|
||||
|
||||
@Schema(description = "创建执行时同步写入的协办人用户编号列表", example = "[3002,3003]")
|
||||
private List<Long> assigneeUserIds;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 执行删除预检 Response VO。
|
||||
* 前端按 hasDependentData 走"简化路径 / 重型路径"分流:
|
||||
* - false(无下挂数据) → 用户二次确认即可
|
||||
* - true → 弹层要求输入执行名称 + 删除口令 + 删除原因
|
||||
*/
|
||||
@Schema(description = "管理后台 - 执行删除预检 Response VO")
|
||||
@Data
|
||||
public class ProjectExecutionDeletePrecheckRespVO {
|
||||
|
||||
@Schema(description = "执行下任务总数(含子孙、含 completed)")
|
||||
private Integer taskCount;
|
||||
|
||||
@Schema(description = "是否存在下挂数据:taskCount > 0 视为 true")
|
||||
private Boolean hasDependentData;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 删除执行 Request VO。与项目侧 ProjectDeleteReqVO 同款三重确认(confirmText / executionName / reason)。
|
||||
* 详见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.2.1。
|
||||
*/
|
||||
@Schema(description = "管理后台 - 执行删除 Request VO")
|
||||
@Data
|
||||
public class ProjectExecutionDeleteReqVO {
|
||||
|
||||
@Schema(description = "确认输入的执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调")
|
||||
@NotBlank(message = "确认执行名称不能为空")
|
||||
@Size(max = 200, message = "确认执行名称长度不能超过200个字符")
|
||||
private String executionName;
|
||||
|
||||
@Schema(description = "删除确认口令,当前固定输入 DELETE", requiredMode = Schema.RequiredMode.REQUIRED, example = "DELETE")
|
||||
@NotBlank(message = "删除确认口令不能为空")
|
||||
@Size(max = 32, message = "删除确认口令长度不能超过32个字符")
|
||||
private String confirmText;
|
||||
|
||||
@Schema(description = "删除原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "执行录入错误")
|
||||
@NotBlank(message = "删除原因不能为空")
|
||||
@Size(max = 500, message = "删除原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "管理后台 - 执行生命周期动作 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class ProjectExecutionLifecycleActionRespVO {
|
||||
|
||||
@Schema(description = "动作编码", example = "pause")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "暂停")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "是否必须填写原因", example = "true")
|
||||
private Boolean needReason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 执行负责人变更 Request VO")
|
||||
@Data
|
||||
public class ProjectExecutionOwnerChangeReqVO {
|
||||
|
||||
@Schema(description = "新负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002")
|
||||
@NotNull(message = "新负责人不能为空")
|
||||
private Long newOwnerId;
|
||||
|
||||
@Schema(description = "变更原因", example = "负责人调整")
|
||||
@Size(max = 500, message = "变更原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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 jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
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 ProjectExecutionPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "关键词,匹配执行名称", example = "联调")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "执行类型", example = "feature")
|
||||
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
||||
private String executionType;
|
||||
|
||||
@Schema(description = "执行负责人用户编号", example = "3001")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "执行状态编码", example = "pending")
|
||||
@Size(max = 32, message = "执行状态编码长度不能超过32个字符")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "优先级字典 rdms_req_priority 值,0=P0(最高) ~ 3=P3(最低)", example = "0")
|
||||
@Size(max = 8)
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 执行 Response VO")
|
||||
@Data
|
||||
public class ProjectExecutionRespVO {
|
||||
|
||||
@Schema(description = "执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
||||
private Long id;
|
||||
@Schema(description = "所属项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001")
|
||||
private Long projectId;
|
||||
@Schema(description = "关联项目需求编号")
|
||||
private Long projectRequirementId;
|
||||
@Schema(description = "关联项目需求名称(service 层批量回填,避免 N+1)", example = "前端联调-需求A")
|
||||
private String projectRequirementName;
|
||||
@Schema(description = "关联项目需求状态编码(service 层批量回填)", example = "implementing")
|
||||
private String projectRequirementStatusCode;
|
||||
@Schema(description = "执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调")
|
||||
private String executionName;
|
||||
@Schema(description = "执行类型", example = "feature")
|
||||
private String executionType;
|
||||
@Schema(description = "执行负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||
private Long ownerId;
|
||||
@Schema(description = "执行负责人昵称", example = "张三")
|
||||
private String ownerNickname;
|
||||
@Schema(description = "执行状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||
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 = "优先级名称(字典 label,后端不填、前端按字典 cache 自译)", example = "P0")
|
||||
private String priorityName;
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
@Schema(description = "当前状态是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
@Schema(description = "当前状态可执行动作")
|
||||
private List<ProjectExecutionLifecycleActionRespVO> availableActions;
|
||||
@Schema(description = "计划开始日期")
|
||||
private LocalDate plannedStartDate;
|
||||
@Schema(description = "计划结束日期")
|
||||
private LocalDate plannedEndDate;
|
||||
@Schema(description = "实际开始日期")
|
||||
private LocalDate actualStartDate;
|
||||
@Schema(description = "实际结束日期")
|
||||
private LocalDate actualEndDate;
|
||||
@Schema(description = "执行进度")
|
||||
private BigDecimal progressRate;
|
||||
@Schema(description = "执行说明")
|
||||
private String executionDesc;
|
||||
@Schema(description = "最近一次状态动作原因")
|
||||
private String lastStatusReason;
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 执行状态动作 Request VO")
|
||||
@Data
|
||||
public class ProjectExecutionStatusActionReqVO {
|
||||
|
||||
@Schema(description = "动作编码,如 start、pause、resume、cancel", requiredMode = Schema.RequiredMode.REQUIRED, example = "pause")
|
||||
@NotBlank(message = "动作编码不能为空")
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作原因;是否必填由状态流转配置决定", example = "依赖环境暂不可用")
|
||||
@Size(max = 500, message = "动作原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
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 static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 执行状态看板 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class ProjectExecutionStatusBoardReqVO {
|
||||
|
||||
@Schema(description = "关键字,匹配执行名称", example = "联调")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "执行类型", example = "feature")
|
||||
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
||||
private String executionType;
|
||||
|
||||
@Schema(description = "执行负责人用户编号", example = "3001")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 执行状态看板 Response VO")
|
||||
@Data
|
||||
public class ProjectExecutionStatusBoardRespVO {
|
||||
|
||||
@Schema(description = "当前筛选条件下的执行总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "状态项列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<ProjectStatusBoardItemVO> items;
|
||||
|
||||
@Schema(description = "项目状态项")
|
||||
@Data
|
||||
public static class ProjectStatusBoardItemVO {
|
||||
|
||||
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||
private String statusCode;
|
||||
@Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "待开始")
|
||||
private String statusName;
|
||||
@Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
private Long count;
|
||||
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Integer sort;
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 编辑执行 Request VO(PUT 全字段语义)。
|
||||
* <p>
|
||||
* 不含 ownerId / assigneeUserIds:换负责人走 /change-owner 独立端点,协办人维护走 /assignees 独立端点。
|
||||
* 详见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.3。
|
||||
*/
|
||||
@Schema(description = "管理后台 - 执行编辑 Request VO")
|
||||
@Data
|
||||
public class ProjectExecutionUpdateReqVO {
|
||||
|
||||
@Schema(description = "执行编号", example = "5001")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "执行名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "后端接口联调")
|
||||
@NotBlank(message = "执行名称不能为空")
|
||||
@Size(max = 200, message = "执行名称长度不能超过200个字符")
|
||||
private String executionName;
|
||||
|
||||
@Schema(description = "执行类型,取值来自字典 rdms_project_execution_type", requiredMode = Schema.RequiredMode.REQUIRED, example = "feature")
|
||||
@NotBlank(message = "执行类型不能为空")
|
||||
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
||||
private String executionType;
|
||||
|
||||
@Schema(description = "优先级字典 rdms_req_priority 值,0=P0(最高) / 1=P1 / 2=P2 / 3=P3(最低)",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotBlank(message = "优先级不能为空")
|
||||
@Size(max = 8, message = "优先级长度不能超过8个字符")
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "关联项目需求编号。PUT 全字段语义:传 null=解绑,传值=更新关联。传入值必须属于本项目且需求非终态", example = "9001")
|
||||
private Long projectRequirementId;
|
||||
|
||||
@Schema(description = "计划开始日期")
|
||||
private LocalDate plannedStartDate;
|
||||
|
||||
@Schema(description = "计划结束日期")
|
||||
private LocalDate plannedEndDate;
|
||||
|
||||
@Schema(description = "执行说明(接受 HTML 富文本,图片走 URL 引用;后端经全局 XSS Safelist 自动净化)",
|
||||
example = "接口联调与问题跟踪")
|
||||
@Size(max = 200000, message = "执行说明长度不能超过200000个字符")
|
||||
private String executionDesc;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateBoardPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryRespVO;
|
||||
import com.njcn.rdms.module.project.service.project.task.ProjectTaskAggregateService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 项目级跨执行任务查询")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/{projectId}/tasks")
|
||||
@Validated
|
||||
public class ProjectTaskAggregateController {
|
||||
|
||||
@Resource
|
||||
private ProjectTaskAggregateService projectTaskAggregateService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取项目级跨执行任务分页")
|
||||
public CommonResult<PageResult<ProjectTaskRespVO>> getTaskPage(
|
||||
@PathVariable("projectId") Long projectId,
|
||||
@Valid ProjectTaskAggregatePageReqVO reqVO) {
|
||||
return success(projectTaskAggregateService.getAggregateTaskPage(projectId, reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/status-board")
|
||||
@Operation(summary = "获取项目级跨执行任务状态看板")
|
||||
public CommonResult<ProjectTaskStatusBoardRespVO> getTaskStatusBoard(
|
||||
@PathVariable("projectId") Long projectId,
|
||||
@Valid ProjectTaskAggregateStatusBoardReqVO reqVO) {
|
||||
return success(projectTaskAggregateService.getAggregateTaskStatusBoard(projectId, reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/board-page")
|
||||
@Operation(summary = "获取项目级跨执行任务看板分页")
|
||||
public CommonResult<ProjectTaskBoardPageRespVO> getTaskBoardPage(
|
||||
@PathVariable("projectId") Long projectId,
|
||||
@Valid ProjectTaskAggregateBoardPageReqVO reqVO) {
|
||||
return success(projectTaskAggregateService.getAggregateTaskBoardPage(projectId, reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/summary")
|
||||
@Operation(summary = "获取项目任务今日小条(支持 scope=mine|all)")
|
||||
public CommonResult<ProjectTaskSummaryRespVO> getTaskSummary(
|
||||
@PathVariable("projectId") Long projectId,
|
||||
@Valid ProjectTaskSummaryReqVO reqVO) {
|
||||
return success(projectTaskAggregateService.getAggregateTaskSummary(projectId, reqVO));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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.ProjectTaskBoardPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskDeletePrecheckRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.service.project.ProjectStatusBoardService;
|
||||
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
||||
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.*;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 项目任务")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/{projectId}/executions/{executionId}/tasks")
|
||||
@Validated
|
||||
public class ProjectTaskController {
|
||||
|
||||
@Resource
|
||||
private ProjectTaskService projectTaskService;
|
||||
@Resource
|
||||
private ProjectStatusBoardService projectStatusBoardService;
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建任务")
|
||||
public CommonResult<Long> createTask(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid @RequestBody ProjectTaskSaveReqVO reqVO) {
|
||||
return success(projectTaskService.createTask(projectId, executionId, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{taskId}")
|
||||
@Operation(summary = "编辑任务")
|
||||
public CommonResult<Boolean> updateTask(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@Valid @RequestBody ProjectTaskSaveReqVO reqVO) {
|
||||
reqVO.setId(taskId);
|
||||
projectTaskService.updateTask(projectId, executionId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}")
|
||||
@Operation(summary = "获取任务详情")
|
||||
public CommonResult<ProjectTaskRespVO> getTask(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId) {
|
||||
return success(projectTaskService.getTaskRespVO(projectId, executionId, taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取任务分页")
|
||||
public CommonResult<PageResult<ProjectTaskRespVO>> getTaskPage(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid ProjectTaskPageReqVO reqVO) {
|
||||
return success(projectTaskService.getTaskRespVOPage(projectId, executionId, reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/status-board")
|
||||
@Operation(summary = "获取任务状态看板")
|
||||
public CommonResult<ProjectTaskStatusBoardRespVO> getTaskStatusBoard(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid ProjectTaskStatusBoardReqVO reqVO) {
|
||||
return success(projectStatusBoardService.getTaskStatusBoard(projectId, executionId, reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/board-page")
|
||||
@Operation(summary = "获取任务看板分页(按状态分列 + 每列分页 + 各列总数)")
|
||||
public CommonResult<ProjectTaskBoardPageRespVO> getTaskBoardPage(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@Valid ProjectTaskBoardPageReqVO reqVO) {
|
||||
return success(projectStatusBoardService.getTaskBoardPage(projectId, executionId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/{taskId}/change-status")
|
||||
@Operation(summary = "变更任务状态")
|
||||
public CommonResult<Boolean> changeStatus(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@Valid @RequestBody ProjectTaskStatusActionReqVO reqVO) {
|
||||
projectTaskService.changeTaskStatus(projectId, executionId, taskId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/delete-precheck")
|
||||
@Operation(summary = "任务删除预检:返回下挂子任务数 + 工作日志数 + 是否需要重型确认")
|
||||
public CommonResult<ProjectTaskDeletePrecheckRespVO> precheckDeleteTask(
|
||||
@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId) {
|
||||
return success(projectTaskService.precheckDeleteTask(projectId, executionId, taskId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{taskId}")
|
||||
@Operation(summary = "删除任务(已完成态禁止,三重确认 + 上级 owner 硬卡)")
|
||||
public CommonResult<Boolean> deleteTask(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@Valid @RequestBody ProjectTaskDeleteReqVO reqVO) {
|
||||
projectTaskService.deleteTask(projectId, executionId, taskId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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.assignee.TaskAssigneeInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeLogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeLogRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeSaveReqVO;
|
||||
import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 任务协办人")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/{projectId}/executions/{executionId}/tasks/{taskId}")
|
||||
@Validated
|
||||
public class TaskAssigneeController {
|
||||
|
||||
@Resource
|
||||
private TaskAssigneeService taskAssigneeService;
|
||||
|
||||
@GetMapping("/assignees")
|
||||
@Operation(summary = "获取任务协办人列表(仅当前活跃)")
|
||||
public CommonResult<List<TaskAssigneeRespVO>> getAssigneeList(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId) {
|
||||
return success(taskAssigneeService.getAssigneeList(projectId, executionId, taskId));
|
||||
}
|
||||
|
||||
@PostMapping("/assignees")
|
||||
@Operation(summary = "加入任务协办人")
|
||||
public CommonResult<Long> createAssignee(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@Valid @RequestBody TaskAssigneeSaveReqVO reqVO) {
|
||||
return success(taskAssigneeService.createAssignee(projectId, executionId, taskId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/assignees/{assigneeId}/inactive")
|
||||
@Operation(summary = "退出任务协办人")
|
||||
public CommonResult<Boolean> inactiveAssignee(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@PathVariable("assigneeId") Long assigneeId,
|
||||
@Valid @RequestBody TaskAssigneeInactiveReqVO reqVO) {
|
||||
taskAssigneeService.inactiveAssignee(projectId, executionId, taskId, assigneeId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/assignee-logs")
|
||||
@Operation(summary = "获取任务协办人变更历史(分页)")
|
||||
public CommonResult<PageResult<TaskAssigneeLogRespVO>> getAssigneeLogPage(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@Valid TaskAssigneeLogPageReqVO reqVO) {
|
||||
return success(taskAssigneeService.getAssigneeLogPage(projectId, executionId, taskId, reqVO));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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.worklog.TaskWorklogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogSaveReqVO;
|
||||
import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogService;
|
||||
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.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 static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 任务工时")
|
||||
@RestController
|
||||
@RequestMapping("/project/project/{projectId}/executions/{executionId}/tasks/{taskId}")
|
||||
@Validated
|
||||
public class TaskWorklogController {
|
||||
|
||||
@Resource
|
||||
private TaskWorklogService taskWorklogService;
|
||||
|
||||
@GetMapping("/worklogs")
|
||||
@Operation(summary = "获取任务工时分页")
|
||||
public CommonResult<PageResult<TaskWorklogRespVO>> getWorklogPage(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@Valid TaskWorklogPageReqVO reqVO) {
|
||||
return success(taskWorklogService.getWorklogPage(projectId, executionId, taskId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/worklogs")
|
||||
@Operation(summary = "新增任务工时")
|
||||
public CommonResult<Long> createWorklog(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||
return success(taskWorklogService.createWorklog(projectId, executionId, taskId, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/worklogs/{worklogId}")
|
||||
@Operation(summary = "修改任务工时(仅自己)")
|
||||
public CommonResult<Boolean> updateWorklog(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@PathVariable("worklogId") Long worklogId,
|
||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||
taskWorklogService.updateWorklog(projectId, executionId, taskId, worklogId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/worklogs/{worklogId}")
|
||||
@Operation(summary = "删除任务工时(自己或任务负责人)")
|
||||
public CommonResult<Boolean> deleteWorklog(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@PathVariable("worklogId") Long worklogId) {
|
||||
taskWorklogService.deleteWorklog(projectId, executionId, taskId, worklogId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.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.LocalDateTime;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* 看板视图任务分页 Request VO。
|
||||
* <p>过滤口径(keyword / parentTaskId / ownerId / updateTime)与 {@link ProjectTaskPageReqVO} 严格一致;
|
||||
* statusCode 升级为数组:缺省=返回该执行下任务状态字典的全部列;传若干个=只返回这些状态的列;
|
||||
* 字典外的 statusCode 静默忽略。pageNo / pageSize 应用到返回的所有列(每列各自分页但页码统一)。
|
||||
*/
|
||||
@Schema(description = "管理后台 - 任务看板分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProjectTaskBoardPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "列选择;缺省返回全部状态列,传若干个只返回这些状态的列;字典外的值静默忽略",
|
||||
example = "[\"pending\",\"active\"]")
|
||||
private String[] statusCode;
|
||||
|
||||
@Schema(description = "优先级字典 rdms_req_priority 值,0=P0(最高) ~ 3=P3(最低)", example = "0")
|
||||
@Size(max = 8)
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "关键词,匹配任务标题", example = "联调")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "父任务编号", example = "9001")
|
||||
private Long parentTaskId;
|
||||
|
||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 看板视图任务分页 Response VO。
|
||||
* <p>每个 item 表示一列:列定义(statusCode/statusName/sort/terminal)+ 当前页切片(list)+ 该列在当前过滤条件下的总数(total)。
|
||||
* list 元素结构与 {@link ProjectTaskRespVO} 完全一致。
|
||||
*/
|
||||
@Schema(description = "管理后台 - 任务看板分页 Response VO")
|
||||
@Data
|
||||
public class ProjectTaskBoardPageRespVO {
|
||||
|
||||
@Schema(description = "列数组(按 sort 升序)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<ColumnItemVO> items;
|
||||
|
||||
@Schema(description = "任务看板单列分页")
|
||||
@Data
|
||||
public static class ColumnItemVO {
|
||||
|
||||
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "待开始")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "排序权重(与 /status-board.items[].sort 同源)", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "10")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
@Schema(description = "该列当前页切片;元素结构与 /tasks/page 的 list 元素一致",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<ProjectTaskRespVO> list;
|
||||
|
||||
@Schema(description = "该列在当前过滤条件下的总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
|
||||
private Long total;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 任务删除预检 Response VO。
|
||||
* 协办人不计入 hasDependentData 判定,但级联删任务时仍会被一并软删。
|
||||
*/
|
||||
@Schema(description = "管理后台 - 任务删除预检 Response VO")
|
||||
@Data
|
||||
public class ProjectTaskDeletePrecheckRespVO {
|
||||
|
||||
@Schema(description = "直接子任务数(不含再下层子孙——前端展示用,判定仍按递归收集后实际删的数量)")
|
||||
private Integer childTaskCount;
|
||||
|
||||
@Schema(description = "工作日志条数")
|
||||
private Integer worklogCount;
|
||||
|
||||
@Schema(description = "是否存在下挂数据:childTaskCount + worklogCount > 0")
|
||||
private Boolean hasDependentData;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 删除任务 Request VO。与执行侧 / 项目侧同款三重确认(confirmText / taskName / reason)。
|
||||
* 详见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.2.2。
|
||||
*/
|
||||
@Schema(description = "管理后台 - 任务删除 Request VO")
|
||||
@Data
|
||||
public class ProjectTaskDeleteReqVO {
|
||||
|
||||
@Schema(description = "确认输入的任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "接口契约对齐")
|
||||
@NotBlank(message = "确认任务名称不能为空")
|
||||
@Size(max = 200, message = "确认任务名称长度不能超过200个字符")
|
||||
private String taskName;
|
||||
|
||||
@Schema(description = "删除确认口令,当前固定输入 DELETE", requiredMode = Schema.RequiredMode.REQUIRED, example = "DELETE")
|
||||
@NotBlank(message = "删除确认口令不能为空")
|
||||
@Size(max = 32, message = "删除确认口令长度不能超过32个字符")
|
||||
private String confirmText;
|
||||
|
||||
@Schema(description = "删除原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "任务录入错误")
|
||||
@NotBlank(message = "删除原因不能为空")
|
||||
@Size(max = 500, message = "删除原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "管理后台 - 任务生命周期动作 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class ProjectTaskLifecycleActionRespVO {
|
||||
|
||||
@Schema(description = "动作编码", example = "complete")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "完成")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "是否必须填写原因", example = "true")
|
||||
private Boolean needReason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.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.LocalDateTime;
|
||||
|
||||
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 ProjectTaskPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "关键词,匹配任务标题", example = "联调")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "父任务编号")
|
||||
private Long parentTaskId;
|
||||
|
||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "任务状态编码", example = "pending")
|
||||
@Size(max = 32, message = "任务状态编码长度不能超过32个字符")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "优先级字典 rdms_req_priority 值,0=P0(最高) ~ 3=P3(最低)", example = "0")
|
||||
@Size(max = 8)
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
|
||||
@Schema(description = "管理后台 - 任务 Response VO")
|
||||
@Data
|
||||
public class ProjectTaskRespVO {
|
||||
|
||||
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
|
||||
private Long id;
|
||||
@Schema(description = "所属项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001")
|
||||
private Long projectId;
|
||||
@Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
||||
private Long executionId;
|
||||
@Schema(description = "所属执行名称", example = "迭代 V1.0")
|
||||
private String executionName;
|
||||
@Schema(description = "所属执行状态码")
|
||||
private String executionStatusCode;
|
||||
@Schema(description = "所属执行关联的项目需求编号(service 层批量回填)")
|
||||
private Long projectRequirementId;
|
||||
@Schema(description = "项目需求名称(service 层批量回填,避免 N+1)", example = "前端联调-需求A")
|
||||
private String projectRequirementName;
|
||||
@Schema(description = "项目需求状态编码(service 层批量回填)", example = "implementing")
|
||||
private String projectRequirementStatusCode;
|
||||
@Schema(description = "父任务编号")
|
||||
private Long parentTaskId;
|
||||
@Schema(description = "父任务负责人用户编号;一级任务为 null,子任务用于前端判断"
|
||||
+ "新增子任务/编辑/删除按钮显隐(详见前端联调清单 §4.3)", example = "3002")
|
||||
private Long parentTaskOwnerId;
|
||||
@Schema(description = "所属执行的负责人用户编号;前端判断一级任务"
|
||||
+ "新增/编辑/删除按钮显隐(一级任务执行负责人可自行调整)", example = "3001")
|
||||
private Long executionOwnerId;
|
||||
@Schema(description = "任务标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "接口联调任务")
|
||||
private String taskTitle;
|
||||
@Schema(description = "任务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "feature")
|
||||
private String type;
|
||||
@Schema(description = "任务负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002")
|
||||
private Long ownerId;
|
||||
@Schema(description = "任务负责人昵称", example = "李四")
|
||||
private String ownerNickname;
|
||||
@Schema(description = "任务状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||
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 = "优先级名称(字典 label,后端不填、前端按字典 cache 自译)", example = "P0")
|
||||
private String priorityName;
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
@Schema(description = "当前状态是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
@Schema(description = "当前状态可执行动作")
|
||||
private List<ProjectTaskLifecycleActionRespVO> availableActions;
|
||||
@Schema(description = "任务进度")
|
||||
private BigDecimal progressRate;
|
||||
@Schema(description = "计划开始日期")
|
||||
private LocalDate plannedStartDate;
|
||||
@Schema(description = "计划结束日期")
|
||||
private LocalDate plannedEndDate;
|
||||
@Schema(description = "实际开始日期")
|
||||
private LocalDate actualStartDate;
|
||||
@Schema(description = "实际结束日期")
|
||||
private LocalDate actualEndDate;
|
||||
@Schema(description = "任务说明")
|
||||
private String taskDesc;
|
||||
@Schema(description = "最近一次状态动作原因")
|
||||
private String lastStatusReason;
|
||||
@Schema(description = "当前活跃协办人列表;详细变更历史见 assignee-logs 接口")
|
||||
private List<TaskAssigneeView> assignees;
|
||||
@Schema(description = "已填报工时合计(小时,0.5 颗粒);逻辑删除的工时记录不计入。无记录默认为 0",
|
||||
example = "8.0")
|
||||
private BigDecimal totalSpentHours;
|
||||
@Schema(description = "附件列表")
|
||||
private List<AttachmentItem> attachments;
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "任务协办人轻量视图")
|
||||
@Data
|
||||
public static class TaskAssigneeView {
|
||||
@Schema(description = "协办关系编号", example = "7001")
|
||||
private Long id;
|
||||
@Schema(description = "协办人用户编号", example = "3002")
|
||||
private Long userId;
|
||||
@Schema(description = "协办人昵称", example = "张三")
|
||||
private String nickname;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 任务保存 Request VO")
|
||||
@Data
|
||||
public class ProjectTaskSaveReqVO {
|
||||
|
||||
@Schema(description = "任务编号", example = "9001")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "父任务编号")
|
||||
private Long parentTaskId;
|
||||
|
||||
@Schema(description = "任务标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "接口联调任务")
|
||||
@NotBlank(message = "任务标题不能为空")
|
||||
@Size(max = 300, message = "任务标题长度不能超过300个字符")
|
||||
private String taskTitle;
|
||||
|
||||
@Schema(description = "任务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "feature")
|
||||
@NotBlank(message = "任务类型不能为空")
|
||||
@Size(max = 32, message = "任务类型长度不能超过32个字符")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "任务负责人用户编号;子任务不传时继承父任务负责人", example = "3002")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "计划开始日期")
|
||||
private LocalDate plannedStartDate;
|
||||
|
||||
@Schema(description = "计划结束日期")
|
||||
private LocalDate plannedEndDate;
|
||||
|
||||
@Schema(description = "任务说明(接受 HTML 富文本,图片走 URL 引用;后端经全局 XSS Safelist 自动净化)",
|
||||
example = "完成接口联调")
|
||||
@Size(max = 200000, message = "任务说明长度不能超过200000个字符")
|
||||
private String taskDesc;
|
||||
|
||||
@Schema(description = "优先级字典 rdms_req_priority 值,0=P0(最高) / 1=P1 / 2=P2 / 3=P3(最低)",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotBlank(message = "优先级不能为空")
|
||||
@Size(max = 8, message = "优先级长度不能超过8个字符")
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "初始协办人用户编号列表;仅在创建任务时生效,编辑任务时静默忽略。"
|
||||
+ "协办人通过独立接口管理,详见 /tasks/{id}/assignees")
|
||||
private List<Long> assigneeUserIds;
|
||||
|
||||
@Schema(description = "附件列表;规则与限制详见 AttachmentValidator(数量上限、扩展名白/黑名单、URL 协议)")
|
||||
@Valid
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 任务状态动作 Request VO")
|
||||
@Data
|
||||
public class ProjectTaskStatusActionReqVO {
|
||||
|
||||
@Schema(description = "动作编码,如 auto_start、pause、resume、complete、cancel", requiredMode = Schema.RequiredMode.REQUIRED, example = "complete")
|
||||
@NotBlank(message = "动作编码不能为空")
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作原因;是否必填由状态流转配置决定", example = "任务取消")
|
||||
@Size(max = 500, message = "动作原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 任务状态看板 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class ProjectTaskStatusBoardReqVO {
|
||||
|
||||
@Schema(description = "关键字,匹配任务标题", example = "联调")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "父任务编号", example = "9001")
|
||||
private Long parentTaskId;
|
||||
|
||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 任务状态看板 Response VO")
|
||||
@Data
|
||||
public class ProjectTaskStatusBoardRespVO {
|
||||
|
||||
@Schema(description = "当前筛选条件下的任务总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "24")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "状态项列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<ProjectStatusBoardItemVO> items;
|
||||
|
||||
@Schema(description = "任务状态项")
|
||||
@Data
|
||||
public static class ProjectStatusBoardItemVO {
|
||||
|
||||
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||
private String statusCode;
|
||||
@Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "待开始")
|
||||
private String statusName;
|
||||
@Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||
private Long count;
|
||||
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Integer sort;
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 项目级跨执行任务看板分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProjectTaskAggregateBoardPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "任务名称模糊匹配关键字")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "限定执行 id 列表;不传 = 项目内全部执行")
|
||||
private List<Long> executionIds;
|
||||
|
||||
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||
private List<String> executionStatusCodes;
|
||||
|
||||
@Schema(description = "我参与语义;与 ownerId 二选一")
|
||||
private Long involveUserId;
|
||||
|
||||
@Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "限定状态列(板上显示哪些列);空 / 不传 = 字典全状态")
|
||||
private List<String> statusCodes;
|
||||
|
||||
@Schema(description = "优先级")
|
||||
@Size(max = 8)
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "父任务 id")
|
||||
private Long parentTaskId;
|
||||
|
||||
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||
private String dueRange;
|
||||
|
||||
@Schema(description = "更新时间范围")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 项目级跨执行任务分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProjectTaskAggregatePageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "任务名称模糊匹配关键字")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "限定执行 id 列表;不传 = 项目内全部执行")
|
||||
private List<Long> executionIds;
|
||||
|
||||
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||
private List<String> executionStatusCodes;
|
||||
|
||||
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一")
|
||||
private Long involveUserId;
|
||||
|
||||
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "状态码多选;空 / 不传 = 全部")
|
||||
private List<String> statusCodes;
|
||||
|
||||
@Schema(description = "优先级字典 value (0~3)")
|
||||
@Size(max = 8)
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "父任务 id(限定到某父任务下)")
|
||||
private Long parentTaskId;
|
||||
|
||||
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||
private String dueRange;
|
||||
|
||||
@Schema(description = "更新时间范围(2 长度数组)")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
|
||||
@Schema(description = "排序字段:plannedEndDate / priority / updateTime / createTime(默认 plannedEndDate)")
|
||||
private String sortBy;
|
||||
|
||||
@Schema(description = "排序方向:asc / desc(默认 asc)")
|
||||
private String sortOrder;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 项目级跨执行任务状态看板 Request VO(入参同 page 去掉分页 / statusCodes / sort)")
|
||||
@Data
|
||||
public class ProjectTaskAggregateStatusBoardReqVO {
|
||||
|
||||
@Schema(description = "任务名称模糊匹配关键字")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "限定执行 id 列表;不传 = 项目内全部执行")
|
||||
private List<Long> executionIds;
|
||||
|
||||
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||
private List<String> executionStatusCodes;
|
||||
|
||||
@Schema(description = "我参与语义;与 ownerId 二选一")
|
||||
private Long involveUserId;
|
||||
|
||||
@Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "优先级")
|
||||
@Size(max = 8)
|
||||
private String priority;
|
||||
|
||||
@Schema(description = "父任务 id")
|
||||
private Long parentTaskId;
|
||||
|
||||
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||
private String dueRange;
|
||||
|
||||
@Schema(description = "更新时间范围")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 项目任务今日小条 Request VO")
|
||||
@Data
|
||||
public class ProjectTaskSummaryReqVO {
|
||||
|
||||
/**
|
||||
* 数字汇总作用域。
|
||||
* <ul>
|
||||
* <li>{@code mine}(默认):统计当前登录人 owner 或活跃协办的任务</li>
|
||||
* <li>{@code all}:统计项目内全部任务,要求 {@code project:task:list-all} 权限码</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Schema(description = "作用域:mine(默认) / all", example = "mine")
|
||||
private String scope;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Schema(description = "管理后台 - 项目任务今日小条 Response VO")
|
||||
@Data
|
||||
public class ProjectTaskSummaryRespVO {
|
||||
|
||||
@Schema(description = "逾期任务数:计划完成日 < 今天,且任务状态非终态")
|
||||
private Long overdue;
|
||||
|
||||
@Schema(description = "今日截止任务数:计划完成日 = 今天,且任务状态非终态")
|
||||
private Long dueToday;
|
||||
|
||||
@Schema(description = "本周到期任务数:计划完成日 ∈ [本周一, 本周日],且任务状态非终态(与 chip thisWeek 过滤同口径)")
|
||||
private Long dueThisWeek;
|
||||
|
||||
@Schema(description = "本周已完成任务数:actualEndDate ∈ [本周一, 今天],且任务状态 = completed")
|
||||
private Long doneThisWeek;
|
||||
|
||||
@Schema(description = "服务器当日(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-22")
|
||||
private LocalDate today;
|
||||
|
||||
@Schema(description = "服务器本周一(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-18")
|
||||
private LocalDate weekStart;
|
||||
|
||||
@Schema(description = "服务器本周日(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-24")
|
||||
private LocalDate weekEnd;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 任务协办人退出 Request VO")
|
||||
@Data
|
||||
public class TaskAssigneeInactiveReqVO {
|
||||
|
||||
@Schema(description = "退出协办的原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "调整到其他任务")
|
||||
@NotBlank(message = "退出协办必须填写原因")
|
||||
@Size(max = 500, message = "原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 任务协办人变更历史分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class TaskAssigneeLogPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "事件类型多选;不传表示全部")
|
||||
private List<String> actionTypes;
|
||||
|
||||
@Schema(description = "成员用户编号;不传表示全部")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "起始时间(含),按 actionTime 比较")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Schema(description = "截止时间(含),按 actionTime 比较")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 任务协办人变更历史 Response VO")
|
||||
@Data
|
||||
public class TaskAssigneeLogRespVO {
|
||||
|
||||
@Schema(description = "日志编号", example = "8001")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "任务编号", example = "9001")
|
||||
private Long taskId;
|
||||
|
||||
@Schema(description = "事件类型:join / inactive", example = "join")
|
||||
private String actionType;
|
||||
|
||||
@Schema(description = "被操作人用户编号", example = "3002")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "被操作人昵称快照")
|
||||
private String userNicknameSnapshot;
|
||||
|
||||
@Schema(description = "操作人用户编号", example = "3001")
|
||||
private Long operatorUserId;
|
||||
|
||||
@Schema(description = "操作人昵称快照")
|
||||
private String operatorNicknameSnapshot;
|
||||
|
||||
@Schema(description = "事件时间")
|
||||
private LocalDateTime actionTime;
|
||||
|
||||
@Schema(description = "原因;inactive 必填,join 可空")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 任务协办人 Response VO")
|
||||
@Data
|
||||
public class TaskAssigneeRespVO {
|
||||
|
||||
@Schema(description = "协办关系编号", example = "7001")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "任务编号", example = "9001")
|
||||
private Long taskId;
|
||||
|
||||
@Schema(description = "协办人用户编号", example = "3002")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "协办人昵称")
|
||||
private String userNickname;
|
||||
|
||||
@Schema(description = "加入时间")
|
||||
private LocalDateTime joinedAt;
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user