This commit is contained in:
dk
2026-05-09 13:36:47 +08:00
142 changed files with 12059 additions and 700 deletions

View File

@@ -0,0 +1,31 @@
{
"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)"
]
}
}

View File

@@ -25,9 +25,13 @@
## 本地环境约定
- 本仓库要求使用 `JDK 17`;不要使用 `JDK 8``JDK 11` 或其他版本执行编译、测试、启动、打包。
- 本机 JDK 17 路径:`C:\Program Files\Java\jdk-17`
- 如需执行 Maven、Java、测试、启动等命令应先确认当前 `JAVA_HOME` 指向 `C:\Program Files\Java\jdk-17`,并确保 `java -version` 实际输出为 `17`
- 本机 Maven 安装路径:`C:\software\apache-maven-3.8.9`
- 如需执行 Maven 命令,优先使用完整路径:`C:\software\apache-maven-3.8.9\bin\mvn.cmd`
- 不要假设 `mvn` 已加入 PATH
- 不要假设系统默认 `JAVA_HOME` 已正确指向 JDK 17如果当前 shell 不是 JDK 17先在当前命令上下文显式切换后再执行 Maven
- 只有在用户已明确同意执行编译、测试、打包等 Maven 命令时,才使用上述路径执行
顶层模块:

110
CLAUDE.md Normal file
View File

@@ -0,0 +1,110 @@
# CLAUDE.md
本文件为 Claude Code 在 `C:\code\gitea\rdms\cn-rdms` 仓库工作时的常驻指引,等价并补充 `AGENTS.md`。两份文件冲突时以本文件为准;本文件未覆盖的细节回退到 `AGENTS.md`
## 工作方式
- 默认先给执行方案:目标、涉及模块、改动点、验证方式。用户明确同意前不要直接动手修改、编译、测试、打包。
- 用户只要分析或评审时,停在分析层;不要顺手开工。
- 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。
- 回答保持精简,先给结论、改动点、必要风险;细节等用户追问。
- **不要废话**:默认极简输出,不展开背景、不复述需求、不堆叠章节标题;能用一两句讲清就别写成清单;用户主动追问再展开。
## 本机环境
- 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 17Spring 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。
- 接口级权限走 `@PreAuthorize("@ss.hasPermission(...)")`,不要绕开。
- 跨模块/跨服务必须通过 `*-api` 模块定义契约;不要直接依赖别人的 `*-boot`
## 数据与 SQL
- 新表 DO 复用现有 `BaseDO` / 审计字段风格,不要再引一套审计基类(除非该表本身明确不需要逻辑删除)。
- **不要假设运行时自动数据库迁移**:依赖新表/新字段/新索引时,必须同步补 SQL 脚本与文档。
- SQL 放在目标模块 `src/main/resources/sql/...`,可审阅、可单独执行。
- 缓存/日志/审计变更优先沿用既有机制,不要绕开登录上下文与审计字段填充。
## 注释与编码
- 关键字段、关键分支、关键约束、非直观实现补**简洁中文**注释。
- 不要为省事删除原有有效注释;也不要写无信息量的注释。
- 中文写入必须 UTF-8并自查显示是否正常不要用"改成英文"规避乱码。
- superpowers 产出的功能文档(设计/实施/联调默认中文落地代码标识、文件路径、接口路径、SQL、命令保持原样不意译。
## 工作规则(执行前对照)
1. 优先做有边界的模块内改动,避免跨模块扩散。
2. 业务逻辑落 `*-boot`;可复用契约落 `*-api`;可复用框架能力落 `rdms-framework`
3. **不要修改** `application-local.yaml` / `application-dev.yaml`,除非任务本身就是环境配置调整。
4. 把本地 YAML 当作可能带机器差异的文件,改前先查 git 状态。
5. 包结构:`controller` / `service` / `dal` / `convert`,保持不变。
6. 新增共享能力优先扩展现有 `rdms-spring-boot-starter-*`,不要在业务服务里重复堆配置。
7. 改跨模块 API 时,提供方实现与对应 `*-api` 契约同步更新。
8. **未经用户明确同意,不执行任何 `mvn`、启动命令、脚本等会实际运行项目的命令。**
## 验证默认动作
先定义验证方式,再做修改。默认静态验证:
- 调用链是否闭环、是否符合模块边界
- 配置项 / 接口契约 / 权限标识 / 路由 / 资源注册前后是否一致
- 改动是否控制在最小集合
- 文档 / SQL / 配置 / 接口说明是否需要同步更新
如果改动涉及 Spring 配置、序列化、安全、路由、RPC 契约、MyBatis 行为或跨模块 API必须**显式区分**哪些是已静态检查、哪些尚未实际运行验证。
## 给后续我自己的提醒
- 仓库可能有未提交的本地改动,不要顺手覆盖与当前任务无关的编辑。
- `docs/` 是当前工作上下文的一部分,不是归档;架构级修改前先查阅。
-`pom.xml` 统一版本与依赖;版本调整改根 pom不要散落到子模块。
- 推荐使用 `Glob` / `Grep` / `Read` 等专用工具,避免用 Bash 做文件搜索/读取/编辑。

View File

@@ -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
#################### 监控相关配置 ####################

View File

@@ -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
#################### 监控相关配置 ####################

View File

@@ -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. 产品经理变更必须提供前后经理信息
这条接口交付后,前端才能把当前“产品动态时间线”从拼装摘要升级成正式可查询模块。

View File

@@ -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 数据源中

View File

@@ -54,4 +54,85 @@ public interface ErrorCodeConstants {
ErrorCode REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_002_015, "存在子模块,请先删除子模块");
ErrorCode REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_002_016, "模块下存在需求,请先删除需求");
// ========== 项目管理 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, "当前产品下已存在未作废的主线项目");
// ========== 执行管理 1-008-003-000 ==========
ErrorCode PROJECT_EXECUTION_NOT_EXISTS = new ErrorCode(1_008_003_000, "执行不存在");
ErrorCode PROJECT_EXECUTION_NAME_DUPLICATE = new ErrorCode(1_008_003_001, "当前项目下已经存在名称为【{}】的执行");
ErrorCode PROJECT_EXECUTION_OWNER_INVALID = new ErrorCode(1_008_003_002, "执行负责人必须是当前项目的有效成员");
ErrorCode PROJECT_EXECUTION_MEMBER_INVALID = new ErrorCode(1_008_003_003, "执行成员必须是当前项目的有效成员");
ErrorCode PROJECT_EXECUTION_MEMBER_ALREADY_EXISTS = new ErrorCode(1_008_003_004, "该用户已是当前执行的有效成员");
ErrorCode PROJECT_EXECUTION_MEMBER_NOT_EXISTS = new ErrorCode(1_008_003_005, "执行成员不存在");
ErrorCode PROJECT_EXECUTION_MEMBER_NOT_ACTIVE = new ErrorCode(1_008_003_006, "当前执行成员已失效");
ErrorCode PROJECT_EXECUTION_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_MEMBER_REQUIRED = new ErrorCode(1_008_003_016, "创建执行时必须至少选择一名执行成员");
// ========== 任务管理 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_PROGRESS_PARENT_NOT_EDITABLE = new ErrorCode(1_008_004_011, "父任务进度由子任务自动汇总,不允许手工修改");
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 且为 30 分钟的整数倍");
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, "仅记录填报人或任务负责人可删除该工时记录");
}

View File

@@ -0,0 +1,18 @@
package com.njcn.rdms.module.project.enums;
/**
* 项目域字典类型常量。
*/
public interface ProjectDictTypeConstants {
/**
* 项目类型。
*/
String PROJECT_TYPE = "rdms_project_type";
/**
* 执行类型。
*/
String EXECUTION_TYPE = "rdms_project_execution_type";
}

View File

@@ -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,18 @@ 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";
// ========== 状态动作 ==========
public static final String STATUS_ACTION_PAUSE = "pause";
public static final String STATUS_ACTION_RESUME = "resume";
@@ -42,6 +55,23 @@ 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";
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_CHANGE_OWNER = "change_execution_owner";
public static final String EXECUTION_MEMBER_ACTION_ADD = "add_execution_member";
public static final String EXECUTION_MEMBER_ACTION_REMOVE = "remove_execution_member";
public static final String TASK_ACTION_CREATE = "create_task_entity";
public static final String TASK_ACTION_UPDATE = "update_task_entity";
// ========== 任务协办人事件类型B 模型 - 多行周期记录) ==========
public static final String TASK_ASSIGNEE_ACTION_JOIN = "join";
public static final String TASK_ASSIGNEE_ACTION_INACTIVE = "inactive";
// ========== 执行成员事件类型B 模型 - 多行周期记录) ==========
public static final String EXECUTION_MEMBER_LOG_ACTION_JOIN = "join";
public static final String EXECUTION_MEMBER_LOG_ACTION_INACTIVE = "inactive";
public static final String EXECUTION_MEMBER_LOG_ACTION_OWNER_TRANSFER_IN = "owner_transfer_in";
public static final String EXECUTION_MEMBER_LOG_ACTION_OWNER_TRANSFER_OUT = "owner_transfer_out";
public static final List<String> STATUS_ACTION_TYPES = List.of(
STATUS_ACTION_PAUSE, STATUS_ACTION_RESUME, STATUS_ACTION_ARCHIVE, STATUS_ACTION_ABANDON);
@@ -67,11 +97,30 @@ 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 EXECUTION_ACTION_CREATE -> "创建执行";
case EXECUTION_ACTION_UPDATE -> "更新执行";
case EXECUTION_ACTION_CHANGE_OWNER -> "变更执行负责人";
case EXECUTION_MEMBER_ACTION_ADD -> "新增执行成员";
case EXECUTION_MEMBER_ACTION_REMOVE -> "移出执行成员";
case TASK_ACTION_CREATE -> "创建任务";
case TASK_ACTION_UPDATE -> "更新任务";
case TASK_ASSIGNEE_ACTION_JOIN -> "加入";
case TASK_ASSIGNEE_ACTION_INACTIVE -> "退出";
case EXECUTION_MEMBER_LOG_ACTION_OWNER_TRANSFER_IN -> "转入负责人";
case EXECUTION_MEMBER_LOG_ACTION_OWNER_TRANSFER_OUT -> "转出负责人";
case "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 -> "移出成员";

View File

@@ -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;
}

View File

@@ -0,0 +1,51 @@
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_DELETE = "project:product:delete";
/**
* 产品删除确认口令。
*/
public static final String DELETE_CONFIRM_TEXT = "DELETE";
}

View File

@@ -0,0 +1,46 @@
package com.njcn.rdms.module.project.constant;
/**
* 执行对象常量。
*/
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_MEMBER = "project:execution:member";
/**
* 执行状态动作权限码。
*/
public static final String PERMISSION_STATUS = "project:execution:status";
}

View File

@@ -0,0 +1,76 @@
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_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);
}

View File

@@ -0,0 +1,46 @@
package com.njcn.rdms.module.project.constant;
/**
* 任务对象常量。
*/
public final class ProjectTaskConstants {
private ProjectTaskConstants() {
}
/**
* 任务对象类型,对应 rdms_object_status_model.object_type。
*/
public static final String OBJECT_TYPE = "task";
/**
* 任务业务类型。
*/
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";
}

View File

@@ -5,6 +5,7 @@ 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.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;
@@ -62,12 +63,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) {

View File

@@ -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;
}

View File

@@ -0,0 +1,90 @@
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.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 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));
}
@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("/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);
}
}

View File

@@ -0,0 +1,63 @@
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.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));
}
@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);
}
}

View File

@@ -0,0 +1,90 @@
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.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.ProjectExecutionSaveReqVO;
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 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 ProjectExecutionSaveReqVO 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 ProjectExecutionSaveReqVO 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) {
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);
}
}

View File

@@ -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.member.ExecutionMemberInactiveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberLogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberLogRespVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberRespVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberSaveReqVO;
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionMemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 执行成员")
@RestController
@RequestMapping("/project/project/{projectId}/executions/{executionId}")
@Validated
public class ProjectExecutionMemberController {
@Resource
private ProjectExecutionMemberService projectExecutionMemberService;
@GetMapping("/members")
@Operation(summary = "获取执行成员列表(仅当前活跃)")
public CommonResult<List<ExecutionMemberRespVO>> getExecutionMemberList(@PathVariable("projectId") Long projectId,
@PathVariable("executionId") Long executionId) {
return success(projectExecutionMemberService.getExecutionMemberList(projectId, executionId));
}
@PostMapping("/members")
@Operation(summary = "新增执行成员B 模型 - 每次新增一段)")
public CommonResult<Long> createExecutionMember(@PathVariable("projectId") Long projectId,
@PathVariable("executionId") Long executionId,
@Valid @RequestBody ExecutionMemberSaveReqVO reqVO) {
return success(projectExecutionMemberService.createExecutionMember(projectId, executionId, reqVO));
}
@PostMapping("/members/{memberId}/inactive")
@Operation(summary = "失效执行成员(永久保留 removedReason")
public CommonResult<Boolean> inactiveExecutionMember(@PathVariable("projectId") Long projectId,
@PathVariable("executionId") Long executionId,
@PathVariable("memberId") Long memberId,
@Valid @RequestBody ExecutionMemberInactiveReqVO reqVO) {
projectExecutionMemberService.inactiveExecutionMember(projectId, executionId, memberId, reqVO);
return success(true);
}
@GetMapping("/member-logs")
@Operation(summary = "获取执行成员变更历史(分页)")
public CommonResult<PageResult<ExecutionMemberLogRespVO>> getExecutionMemberLogPage(
@PathVariable("projectId") Long projectId,
@PathVariable("executionId") Long executionId,
@Valid ExecutionMemberLogPageReqVO reqVO) {
return success(projectExecutionMemberService.getExecutionMemberLogPage(projectId, executionId, reqVO));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,37 @@
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 = "更新时间", 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;
}

View File

@@ -0,0 +1,58 @@
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 = "执行名称", 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 = "是否终态", 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;
}

View File

@@ -0,0 +1,50 @@
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;
@Schema(description = "管理后台 - 执行保存 Request VO")
@Data
public class ProjectExecutionSaveReqVO {
@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 = "执行负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
@NotNull(message = "执行负责人不能为空")
private Long ownerId;
@Schema(description = "关联项目需求编号,第一阶段只接受空值", example = "9001")
private Long projectRequirementId;
@Schema(description = "计划开始日期")
private LocalDate plannedStartDate;
@Schema(description = "计划结束日期")
private LocalDate plannedEndDate;
@Schema(description = "执行说明(接受 HTML 富文本,图片走 URL 引用;后端经全局 XSS Safelist 自动净化)",
example = "接口联调与问题跟踪")
@Size(max = 200000, message = "执行说明长度不能超过200000个字符")
private String executionDesc;
@Schema(description = "创建执行时同步写入的成员用户编号列表;编辑执行主数据时不维护成员", example = "[3002,3003]")
private List<Long> memberUserIds;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,17 @@
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member;
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 ExecutionMemberInactiveReqVO {
@Schema(description = "失效原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "阶段性退出")
@NotBlank(message = "失效原因不能为空")
@Size(max = 500, message = "失效原因长度不能超过500个字符")
private String reason;
}

View File

@@ -0,0 +1,29 @@
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member;
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 ExecutionMemberLogPageReqVO 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;
}

View File

@@ -0,0 +1,39 @@
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 执行成员变更历史 Response VO")
@Data
public class ExecutionMemberLogRespVO {
@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;
}

View File

@@ -0,0 +1,27 @@
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 执行成员 Response VO")
@Data
public class ExecutionMemberRespVO {
@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;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.project.execution.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 执行成员新增 Request VO")
@Data
public class ExecutionMemberSaveReqVO {
@Schema(description = "成员用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002")
@NotNull(message = "成员用户不能为空")
private Long userId;
}

View File

@@ -0,0 +1,86 @@
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.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));
}
@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);
}
}

View File

@@ -0,0 +1,76 @@
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.security.access.prepost.PreAuthorize;
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 = "加入任务协办人")
@PreAuthorize("@ss.hasPermission('project:task:assignee')")
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 = "退出任务协办人")
@PreAuthorize("@ss.hasPermission('project:task:assignee')")
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));
}
}

View File

@@ -0,0 +1,77 @@
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.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import 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 = "新增任务工时")
@PreAuthorize("@ss.hasPermission('project:task:worklog')")
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 = "修改任务工时(仅自己)")
@PreAuthorize("@ss.hasPermission('project:task:worklog')")
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 = "删除任务工时(自己或任务负责人)")
@PreAuthorize("@ss.hasPermission('project:task:worklog')")
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);
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,36 @@
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 = "更新时间", 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;
}

View File

@@ -0,0 +1,74 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
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 = "父任务编号")
private Long parentTaskId;
@Schema(description = "任务标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "接口联调任务")
private String taskTitle;
@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 = "是否终态", 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",
example = "300")
private Long totalSpentMinutes;
@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;
}
}

View File

@@ -0,0 +1,53 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo;
import io.swagger.v3.oas.annotations.media.Schema;
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 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 = "任务负责人用户编号;子任务不传时继承父任务负责人", example = "3002")
private Long ownerId;
@Schema(description = "任务进度", example = "0.00")
@DecimalMin(value = "0.00", message = "任务进度不能小于0")
@DecimalMax(value = "100.00", message = "任务进度不能大于100")
private BigDecimal progressRate;
@Schema(description = "计划开始日期")
private LocalDate plannedStartDate;
@Schema(description = "计划结束日期")
private LocalDate plannedEndDate;
@Schema(description = "任务说明(接受 HTML 富文本,图片走 URL 引用;后端经全局 XSS Safelist 自动净化)",
example = "完成接口联调")
@Size(max = 200000, message = "任务说明长度不能超过200000个字符")
private String taskDesc;
@Schema(description = "初始协办人用户编号列表;仅在创建任务时生效,编辑任务时静默忽略。"
+ "协办人通过独立接口管理,详见 /tasks/{id}/assignees")
private List<Long> assigneeUserIds;
}

View File

@@ -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 = "动作编码,如 start、block、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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.project.task.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 TaskAssigneeSaveReqVO {
@Schema(description = "协办人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3002")
@NotNull(message = "协办人用户编号不能为空")
private Long userId;
}

View File

@@ -0,0 +1,24 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog;
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.LocalDate;
@Schema(description = "管理后台 - 任务工时分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class TaskWorklogPageReqVO extends PageParam {
@Schema(description = "填报人用户编号;不传表示全部")
private Long userId;
@Schema(description = "起始日期(含),按 workDate 比较")
private LocalDate startDate;
@Schema(description = "截止日期(含),按 workDate 比较")
private LocalDate endDate;
}

View File

@@ -0,0 +1,40 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 任务工时 Response VO")
@Data
public class TaskWorklogRespVO {
@Schema(description = "工时记录编号", example = "11001")
private Long id;
@Schema(description = "任务编号", example = "9001")
private Long taskId;
@Schema(description = "填报人用户编号", example = "3002")
private Long userId;
@Schema(description = "填报人昵称", example = "张三")
private String userNickname;
@Schema(description = "工作日期", example = "2026-05-08")
private LocalDate workDate;
@Schema(description = "时长(分钟)", example = "150")
private Integer durationMinutes;
@Schema(description = "工作内容描述")
private String workContent;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,33 @@
package com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
/**
* 任务工时新增/更新请求。同表共用updateWorklog 不接受 taskId / userId 切换,前端无需也无法传。
* 时长颗粒30 分钟整数倍)由 Service 层校验。
*/
@Schema(description = "管理后台 - 任务工时 Save Request VO")
@Data
public class TaskWorklogSaveReqVO {
@Schema(description = "工作日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-08")
@NotNull(message = "工作日期不能为空")
private LocalDate workDate;
@Schema(description = "时长(分钟),> 0 且必须为 30 的整数倍",
requiredMode = Schema.RequiredMode.REQUIRED, example = "150")
@NotNull(message = "工时时长不能为空")
@Min(value = 30, message = "工时时长必须大于 0 且为 30 分钟的整数倍")
private Integer durationMinutes;
@Schema(description = "工作内容描述", example = "完成接口联调与冒烟测试")
@Size(max = 2000, message = "工作内容长度不能超过 2000 个字符")
private String workContent;
}

View File

@@ -0,0 +1,17 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.member;
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 ProjectMemberInactiveReqVO {
@Schema(description = "移出原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "已退出当前项目协作")
@NotBlank(message = "移出原因不能为空")
@Size(max = 500, message = "移出原因长度不能超过500个字符")
private String reason;
}

View File

@@ -0,0 +1,35 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 项目成员 Response VO")
@Data
public class ProjectMemberRespVO {
@Schema(description = "成员关系编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long userId;
@Schema(description = "用户昵称", example = "小王")
private String userNickname;
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002001")
private Long roleId;
@Schema(description = "角色名称", example = "项目经理")
private String roleName;
@Schema(description = "角色编码", example = "project_manager")
private String roleCode;
@Schema(description = "是否当前项目负责人", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean managerFlag;
@Schema(description = "状态0有效 1失效", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;
@Schema(description = "加入时间")
private LocalDateTime joinedTime;
@Schema(description = "退出时间")
private LocalDateTime leftTime;
@Schema(description = "备注", example = "当前负责需求收敛")
private String remark;
}

View File

@@ -0,0 +1,30 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.member;
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 ProjectMemberSaveReqVO {
@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;
@Schema(description = "原项目经理用户编号,仅切换项目经理时传递", example = "2048")
private Long previousManagerUserId;
@Schema(description = "原项目经理交接后的角色编号,仅切换项目经理时传递", example = "3100000002002")
private Long previousManagerRoleId;
}

View File

@@ -0,0 +1,30 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.member;
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 ProjectMemberUpdateReqVO {
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002002")
@NotNull(message = "角色编号不能为空")
private Long roleId;
@Schema(description = "变更原因", example = "职责调整")
@Size(max = 500, message = "变更原因长度不能超过500个字符")
private String reason;
@Schema(description = "备注", example = "调整为项目观察者")
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
@Schema(description = "原项目经理用户编号,仅切换项目经理时传递", example = "2048")
private Long previousManagerUserId;
@Schema(description = "原项目经理交接后的角色编号,仅切换项目经理时传递", example = "3100000002002")
private Long previousManagerRoleId;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 项目上下文导航 Response VO")
@Data
public class ProjectContextNavRespVO {
@Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3201")
private Long id;
@Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "概览")
private String name;
@Schema(description = "菜单路径", example = "/project/project/overview")
private String path;
@Schema(description = "菜单图标", example = "mdi:view-dashboard-outline")
private String icon;
@Schema(description = "显示顺序", example = "10")
private Integer sort;
}

View File

@@ -0,0 +1,37 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 项目上下文中的当前项目摘要 Response VO")
@Data
public class ProjectContextProjectRespVO {
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "项目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPJ2026001")
private String projectCode;
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户交付项目")
private String projectName;
@Schema(description = "项目类型字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "delivery")
private String projectType;
@Schema(description = "项目方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
private String directionCode;
@Schema(description = "所属产品编号")
private Long productId;
@Schema(description = "项目负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long managerUserId;
@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<ProjectLifecycleActionRespVO> availableActions;
}

View File

@@ -0,0 +1,23 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Schema(description = "管理后台 - 项目上下文 Response VO")
@Data
@NoArgsConstructor
public class ProjectContextRespVO {
@Schema(description = "当前项目摘要")
private ProjectContextProjectRespVO currentProject;
@Schema(description = "当前用户在该项目下的角色信息")
private ProjectContextRoleRespVO currentRole;
@Schema(description = "当前项目下可见导航集合")
private List<ProjectContextNavRespVO> navs;
@Schema(description = "当前项目下按钮权限码集合")
private List<String> buttons;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 项目上下文中的当前角色 Response VO")
@Data
public class ProjectContextRoleRespVO {
@Schema(description = "对象角色编号", example = "3201")
private Long roleId;
@Schema(description = "对象角色编码", example = "project_manager")
private String roleCode;
@Schema(description = "对象角色名称", example = "项目经理")
private String roleName;
@Schema(description = "是否游客上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean guestFlag;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
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;
@Schema(description = "管理后台 - 项目删除 Request VO")
@Data
public class ProjectDeleteReqVO {
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "项目编号不能为空")
private Long id;
@Schema(description = "确认输入的项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户交付项目")
@NotBlank(message = "确认项目名称不能为空")
@Size(max = 200, message = "确认项目名称长度不能超过200个字符")
private String projectName;
@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;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
@Schema(description = "管理后台 - 项目生命周期动作 Response VO")
@Data
@NoArgsConstructor
public class ProjectLifecycleActionRespVO {
@Schema(description = "动作编码", example = "archive")
private String actionCode;
@Schema(description = "动作名称", example = "归档")
private String actionName;
@Schema(description = "是否必须填写原因", example = "true")
private Boolean needReason;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Map;
@Schema(description = "管理后台 - 项目入口页概览统计 Response VO")
@Data
public class ProjectOverviewSummaryRespVO {
@Schema(description = "项目状态数量,按当前启用的项目状态模型返回")
private Map<String, Long> statusCounts;
}

View File

@@ -0,0 +1,46 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.dict.validation.InDict;
import com.njcn.rdms.module.system.enums.DictTypeConstants;
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 ProjectPageReqVO extends PageParam {
@Schema(description = "关键词,匹配项目编码或项目名称", example = "CNPJ2026001")
private String keyword;
@Schema(description = "项目类型字典值", example = "delivery")
@Size(max = 32, message = "项目类型长度不能超过32个字符")
private String projectType;
@Schema(description = "项目方向字典值", example = "direction_value")
@InDict(type = DictTypeConstants.OBJECT_DIRECTION)
private String directionCode;
@Schema(description = "所属产品编号", example = "1001")
private Long productId;
@Schema(description = "项目负责人用户编号", example = "1024")
private Long managerUserId;
@Schema(description = "项目状态编码", example = "active")
@Size(max = 32, message = "项目状态编码长度不能超过32个字符")
private String statusCode;
@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;
}

View File

@@ -0,0 +1,57 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 项目 Response VO")
@Data
public class ProjectRespVO {
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "项目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPJ2026001")
private String projectCode;
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户交付项目")
private String projectName;
@Schema(description = "项目类型字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "delivery")
private String projectType;
@Schema(description = "项目方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
private String directionCode;
@Schema(description = "所属项目集编号")
private Long projectSetId;
@Schema(description = "所属产品编号")
private Long productId;
@Schema(description = "所属产品名称", example = "统一交付平台")
private String productName;
@Schema(description = "所属产品版本编号")
private Long productVersionId;
@Schema(description = "项目负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long managerUserId;
@Schema(description = "项目负责人昵称", example = "张三")
private String managerUserNickname;
@Schema(description = "项目状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
private String statusCode;
@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 projectDesc;
@Schema(description = "最近一次状态动作原因")
private String lastStatusReason;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,64 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
@Schema(description = "管理后台 - 项目保存 Request VO")
@Data
public class ProjectSaveReqVO {
@Schema(description = "项目编号", example = "1024")
private Long id;
@Schema(description = "项目编码,为空时由系统自动生成", example = "CNPJ2026001")
@Size(max = 64, message = "项目编码长度不能超过64个字符")
private String projectCode;
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户交付项目")
@NotBlank(message = "项目名称不能为空")
@Size(max = 200, message = "项目名称长度不能超过200个字符")
private String projectName;
@Schema(description = "项目类型字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "delivery")
@NotBlank(message = "项目类型不能为空")
@Size(max = 32, message = "项目类型长度不能超过32个字符")
private String projectType;
@Schema(description = "项目方向字典值;未选产品时必填,选产品时以后端产品方向为准", example = "direction_value")
private String directionCode;
@Schema(description = "所属项目集编号,第一期预留", example = "1001")
private Long projectSetId;
@Schema(description = "所属产品编号,可为空", example = "1001")
private Long productId;
@Schema(description = "所属产品版本编号,第一期预留", example = "1001")
private Long productVersionId;
@Schema(description = "项目负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "项目负责人不能为空")
private Long managerUserId;
@Schema(description = "计划开始日期")
private LocalDate plannedStartDate;
@Schema(description = "计划结束日期")
private LocalDate plannedEndDate;
@Schema(description = "实际开始日期")
private LocalDate actualStartDate;
@Schema(description = "实际结束日期")
private LocalDate actualEndDate;
@Schema(description = "项目说明", example = "客户定制交付")
@Size(max = 4000, message = "项目说明长度不能超过4000个字符")
private String projectDesc;
}

View File

@@ -0,0 +1,26 @@
package com.njcn.rdms.module.project.controller.admin.project.vo.project;
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;
@Schema(description = "管理后台 - 项目状态动作 Request VO")
@Data
public class ProjectStatusActionReqVO {
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "项目编号不能为空")
private Long id;
@Schema(description = "动作编码,如 pause、resume、complete、cancel、reopen、archive", 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;
}

View File

@@ -0,0 +1,90 @@
package com.njcn.rdms.module.project.dal.dataobject.project;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 项目主表
*/
@TableName("rdms_project")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectDO extends BaseDO {
/**
* 项目编号
*/
@TableId
private Long id;
/**
* 项目编码
*/
private String projectCode;
/**
* 项目名称
*/
private String projectName;
/**
* 项目类型字典值
*/
private String projectType;
/**
* 项目方向字典值
*/
private String directionCode;
/**
* 所属项目集编号,第一期预留
*/
private Long projectSetId;
/**
* 所属产品编号,可为空
*/
private Long productId;
/**
* 所属产品版本编号,第一期预留
*/
private Long productVersionId;
/**
* 项目负责人用户编号
*/
private Long managerUserId;
/**
* 项目状态编码
*/
private String statusCode;
/**
* 计划开始日期
*/
private LocalDate plannedStartDate;
/**
* 计划结束日期
*/
private LocalDate plannedEndDate;
/**
* 实际开始日期
*/
private LocalDate actualStartDate;
/**
* 实际结束日期
*/
private LocalDate actualEndDate;
/**
* 项目进度缓存值
*/
private BigDecimal progressRate;
/**
* 项目说明
*/
private String projectDesc;
/**
* 最近一次状态动作原因
*/
private String lastStatusReason;
}

View File

@@ -0,0 +1,63 @@
package com.njcn.rdms.module.project.dal.dataobject.project;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 项目状态日志表
*/
@TableName("rdms_project_status_log")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectStatusLogDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 项目ID
*/
private Long projectId;
/**
* 动作类型
*/
private String actionType;
/**
* 变更前状态编码
*/
private String fromStatus;
/**
* 变更后状态编码
*/
private String toStatus;
/**
* 动作原因
*/
private String reason;
/**
* 操作人用户ID
*/
private Long operatorUserId;
/**
* 操作人名称快照
*/
private String operatorName;
/**
* 项目编码快照
*/
private String projectCodeSnapshot;
/**
* 项目名称快照
*/
private String projectNameSnapshot;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,45 @@
package com.njcn.rdms.module.project.dal.dataobject.project.execution;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 执行成员关系表。
*/
@TableName("rdms_execution_member")
@Data
@EqualsAndHashCode(callSuper = true)
public class ExecutionMemberDO extends BaseDO {
/**
* 主键编号
*/
@TableId
private Long id;
/**
* 所属执行编号
*/
private Long executionId;
/**
* 成员用户编号
*/
private Long userId;
/**
* 加入时间
*/
private LocalDateTime joinedAt;
/**
* 失效时间,为空表示当前有效
*/
private LocalDateTime removedAt;
/**
* 失效原因
*/
private String removedReason;
}

View File

@@ -0,0 +1,58 @@
package com.njcn.rdms.module.project.dal.dataobject.project.execution;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 执行成员变更历史日志B 模型 - 全量事件记录)。
* 每次 join / inactive / owner_transfer_in / owner_transfer_out 独立成一条记录,昵称展示由查询阶段按用户编号回填。
*/
@TableName("rdms_execution_member_log")
@Data
@EqualsAndHashCode(callSuper = true)
public class ExecutionMemberLogDO extends BaseDO {
/**
* 主键 ID
*/
@TableId
private Long id;
/**
* 所属执行编号
*/
private Long executionId;
/**
* 被操作的成员用户编号
*/
private Long userId;
/**
* 被操作人昵称冗余字段。当前业务写入不再落昵称,返回时按 userId 实时回填。
*/
private String userNicknameSnapshot;
/**
* 事件类型join / inactive / owner_transfer_in / owner_transfer_out
*/
private String actionType;
/**
* 事件原因inactive 必填,其余可空
*/
private String reason;
/**
* 操作人用户编号
*/
private Long operatorUserId;
/**
* 操作人昵称冗余字段。当前业务写入不再落昵称,返回时按 operatorUserId 实时回填。
*/
private String operatorNicknameSnapshot;
/**
* 事件时间
*/
private LocalDateTime actionTime;
}

View File

@@ -0,0 +1,78 @@
package com.njcn.rdms.module.project.dal.dataobject.project.execution;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 项目执行主表。
*/
@TableName("rdms_project_execution")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectExecutionDO extends BaseDO {
/**
* 执行编号
*/
@TableId
private Long id;
/**
* 所属项目编号
*/
private Long projectId;
/**
* 关联项目需求编号,第一阶段仅保留字段
*/
private Long projectRequirementId;
/**
* 执行名称
*/
private String executionName;
/**
* 执行类型
*/
private String executionType;
/**
* 执行负责人用户编号
*/
private Long ownerId;
/**
* 执行状态编码
*/
private String statusCode;
/**
* 计划开始日期
*/
private LocalDate plannedStartDate;
/**
* 计划结束日期
*/
private LocalDate plannedEndDate;
/**
* 实际开始日期
*/
private LocalDate actualStartDate;
/**
* 实际结束日期
*/
private LocalDate actualEndDate;
/**
* 执行进度缓存值
*/
private BigDecimal progressRate;
/**
* 执行说明
*/
private String executionDesc;
/**
* 最近一次状态动作原因
*/
private String lastStatusReason;
}

View File

@@ -0,0 +1,59 @@
package com.njcn.rdms.module.project.dal.dataobject.project.execution;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 执行状态日志表。
*/
@TableName("rdms_execution_status_log")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectExecutionStatusLogDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 执行ID
*/
private Long executionId;
/**
* 动作编码
*/
private String actionType;
/**
* 变更前状态编码
*/
private String fromStatus;
/**
* 变更后状态编码
*/
private String toStatus;
/**
* 动作原因
*/
private String reason;
/**
* 操作人用户ID
*/
private Long operatorUserId;
/**
* 操作人名称快照
*/
private String operatorName;
/**
* 执行名称快照
*/
private String executionNameSnapshot;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,78 @@
package com.njcn.rdms.module.project.dal.dataobject.project.task;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 项目任务主表。
*/
@TableName("rdms_task")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectTaskDO extends BaseDO {
/**
* 任务编号
*/
@TableId
private Long id;
/**
* 所属项目编号
*/
private Long projectId;
/**
* 所属执行编号
*/
private Long executionId;
/**
* 父任务编号
*/
private Long parentTaskId;
/**
* 任务标题
*/
private String taskTitle;
/**
* 任务负责人用户编号
*/
private Long ownerId;
/**
* 任务状态编码
*/
private String statusCode;
/**
* 任务进度
*/
private BigDecimal progressRate;
/**
* 计划开始日期
*/
private LocalDate plannedStartDate;
/**
* 计划结束日期
*/
private LocalDate plannedEndDate;
/**
* 实际开始日期
*/
private LocalDate actualStartDate;
/**
* 实际结束日期
*/
private LocalDate actualEndDate;
/**
* 任务说明
*/
private String taskDesc;
/**
* 最近一次状态动作原因
*/
private String lastStatusReason;
}

View File

@@ -0,0 +1,59 @@
package com.njcn.rdms.module.project.dal.dataobject.project.task;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 任务状态日志表。
*/
@TableName("rdms_task_status_log")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProjectTaskStatusLogDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 任务ID
*/
private Long taskId;
/**
* 动作编码
*/
private String actionType;
/**
* 变更前状态编码
*/
private String fromStatus;
/**
* 变更后状态编码
*/
private String toStatus;
/**
* 动作原因
*/
private String reason;
/**
* 操作人用户ID
*/
private Long operatorUserId;
/**
* 操作人名称快照
*/
private String operatorName;
/**
* 任务标题快照
*/
private String taskTitleSnapshot;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,43 @@
package com.njcn.rdms.module.project.dal.dataobject.project.task;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 任务协办人活跃记录表B 模型 - 多行周期记录)。
* 同一 userId 在同一任务内允许多条历史记录,但任意时刻只有一段 removedAt 为 null活跃
* 失效时设置 removedAt不动 BaseDO.deleted失效原因和时间快照同步写入 {@link TaskAssigneeLogDO}。
*/
@TableName("rdms_task_assignee")
@Data
@EqualsAndHashCode(callSuper = true)
public class TaskAssigneeDO extends BaseDO {
/**
* 主键 ID
*/
@TableId
private Long id;
/**
* 任务编号
*/
private Long taskId;
/**
* 协办人用户编号
*/
private Long userId;
/**
* 加入时间
*/
private LocalDateTime joinedAt;
/**
* 失效时间,为 null 表示当前活跃
*/
private LocalDateTime removedAt;
}

View File

@@ -0,0 +1,58 @@
package com.njcn.rdms.module.project.dal.dataobject.project.task;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 任务协办人变更历史日志表B 模型 - 全量事件记录)。
* 每次 join / inactive 都独立成一条记录nickname 在写入时快照不再回查。
*/
@TableName("rdms_task_assignee_log")
@Data
@EqualsAndHashCode(callSuper = true)
public class TaskAssigneeLogDO extends BaseDO {
/**
* 主键 ID
*/
@TableId
private Long id;
/**
* 任务编号
*/
private Long taskId;
/**
* 被操作的协办人用户编号
*/
private Long userId;
/**
* 被操作人昵称快照
*/
private String userNicknameSnapshot;
/**
* 事件类型join / inactive
*/
private String actionType;
/**
* 原因inactive 必填join 可空
*/
private String reason;
/**
* 操作人用户编号
*/
private Long operatorUserId;
/**
* 操作人昵称快照
*/
private String operatorNicknameSnapshot;
/**
* 事件时间
*/
private LocalDateTime actionTime;
}

View File

@@ -0,0 +1,46 @@
package com.njcn.rdms.module.project.dal.dataobject.project.task;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
/**
* 任务工时记录表。仅挂在叶子任务上;同一 user × task × work_date 允许多条。
* 时长按分钟存duration_minutes 必须 > 0 且为 30 的整数倍),前端展示为小时。
*/
@TableName("rdms_task_worklog")
@Data
@EqualsAndHashCode(callSuper = true)
public class TaskWorklogDO extends BaseDO {
/**
* 主键 ID
*/
@TableId
private Long id;
/**
* 任务编号(必须为叶子任务)
*/
private Long taskId;
/**
* 填报人用户编号owner 或在岗协办人)
*/
private Long userId;
/**
* 工作日期
*/
private LocalDate workDate;
/**
* 时长(分钟为单位),必须 > 0 且为 30 的整数倍
*/
private Integer durationMinutes;
/**
* 工作内容描述
*/
private String workContent;
}

View File

@@ -52,4 +52,11 @@ public interface UserObjectRoleMapper extends BaseMapperX<UserObjectRoleDO> {
.eq(UserObjectRoleDO::getObjectId, objectId));
}
default List<UserObjectRoleDO> selectActiveListByObjectTypeAndUserId(String objectType, Long userId) {
return selectList(new LambdaQueryWrapperX<UserObjectRoleDO>()
.eq(UserObjectRoleDO::getObjectType, objectType)
.eq(UserObjectRoleDO::getUserId, userId)
.eq(UserObjectRoleDO::getStatus, 0));
}
}

View File

@@ -7,9 +7,11 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
@Mapper
public interface ProductMapper extends BaseMapperX<ProductDO> {
@@ -25,7 +27,7 @@ public interface ProductMapper extends BaseMapperX<ProductDO> {
.eqIfPresent(ProductDO::getManagerUserId, reqVO.getManagerUserId())
.eqIfPresent(ProductDO::getStatusCode, reqVO.getStatusCode())
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime())
.orderByDesc(BaseDO::getUpdateTime);
.orderByDesc(BaseDO::getCreateTime);
return selectPage(reqVO, queryWrapper);
}
@@ -43,6 +45,14 @@ public interface ProductMapper extends BaseMapperX<ProductDO> {
.orderByDesc(ProductDO::getCode));
}
@Select("""
SELECT status_code AS statusCode, COUNT(*) AS countValue
FROM rdms_product
WHERE deleted = b'0'
GROUP BY status_code
""")
List<Map<String, Object>> selectStatusCountList();
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
ProductDO update = new ProductDO();
update.setStatusCode(toStatus);

View File

@@ -0,0 +1,90 @@
package com.njcn.rdms.module.project.dal.mysql.project;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@Mapper
public interface ProjectMapper extends BaseMapperX<ProjectDO> {
default PageResult<ProjectDO> selectPage(ProjectPageReqVO reqVO) {
LambdaQueryWrapperX<ProjectDO> queryWrapper = new LambdaQueryWrapperX<>();
if (StringUtils.hasText(reqVO.getKeyword())) {
queryWrapper.and(wrapper -> wrapper.like(ProjectDO::getProjectCode, reqVO.getKeyword())
.or()
.like(ProjectDO::getProjectName, reqVO.getKeyword()));
}
queryWrapper.eqIfPresent(ProjectDO::getProjectType, reqVO.getProjectType())
.eqIfPresent(ProjectDO::getDirectionCode, reqVO.getDirectionCode())
.eqIfPresent(ProjectDO::getProductId, reqVO.getProductId())
.eqIfPresent(ProjectDO::getManagerUserId, reqVO.getManagerUserId())
.eqIfPresent(ProjectDO::getStatusCode, reqVO.getStatusCode())
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime())
.orderByDesc(BaseDO::getCreateTime);
return selectPage(reqVO, queryWrapper);
}
default ProjectDO selectByCode(String projectCode) {
return selectOne(ProjectDO::getProjectCode, projectCode);
}
default ProjectDO selectActiveByProductIdAndName(Long productId, String projectName, String excludedStatusCode) {
LambdaQueryWrapperX<ProjectDO> queryWrapper = new LambdaQueryWrapperX<>();
queryWrapper.eq(ProjectDO::getProjectName, projectName)
.ne(ProjectDO::getStatusCode, excludedStatusCode);
if (productId == null) {
queryWrapper.isNull(ProjectDO::getProductId);
} else {
queryWrapper.eq(ProjectDO::getProductId, productId);
}
return selectOne(queryWrapper);
}
default List<ProjectDO> selectActiveMainlineListByProductId(Long productId, Collection<String> projectTypes,
String excludedStatusCode) {
return selectList(new LambdaQueryWrapperX<ProjectDO>()
.eq(ProjectDO::getProductId, productId)
.in(ProjectDO::getProjectType, projectTypes)
.ne(ProjectDO::getStatusCode, excludedStatusCode));
}
@Select("""
SELECT status_code AS statusCode, COUNT(*) AS countValue
FROM rdms_project
WHERE deleted = b'0'
GROUP BY status_code
""")
List<Map<String, Object>> selectStatusCountList();
default List<ProjectDO> selectListByCodePrefix(String codePrefix) {
return selectList(new LambdaQueryWrapperX<ProjectDO>()
.likeRight(ProjectDO::getProjectCode, codePrefix)
.orderByDesc(ProjectDO::getProjectCode));
}
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
ProjectDO update = new ProjectDO();
update.setStatusCode(toStatus);
update.setLastStatusReason(lastStatusReason);
return update(update, new LambdaQueryWrapperX<ProjectDO>()
.eq(ProjectDO::getId, id)
.eq(ProjectDO::getStatusCode, fromStatus));
}
default int deleteByIdAndStatus(Long id, String statusCode) {
return delete(new LambdaQueryWrapperX<ProjectDO>()
.eq(ProjectDO::getId, id)
.eq(ProjectDO::getStatusCode, statusCode));
}
}

View File

@@ -0,0 +1,24 @@
package com.njcn.rdms.module.project.dal.mysql.project;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectStatusLogDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface ProjectStatusLogMapper extends BaseMapperX<ProjectStatusLogDO> {
default List<ProjectStatusLogDO> selectListByProjectId(Long projectId, String actionType, LocalDateTime[] operateTime) {
return selectList(new LambdaQueryWrapperX<ProjectStatusLogDO>()
.eq(ProjectStatusLogDO::getProjectId, projectId)
.eqIfPresent(ProjectStatusLogDO::getActionType, actionType)
.betweenIfPresent(BaseDO::getCreateTime, operateTime)
.orderByDesc(BaseDO::getCreateTime)
.orderByDesc(ProjectStatusLogDO::getId));
}
}

View File

@@ -0,0 +1,30 @@
package com.njcn.rdms.module.project.dal.mysql.project.execution;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.member.ExecutionMemberLogPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionMemberLogDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ExecutionMemberLogMapper extends BaseMapperX<ExecutionMemberLogDO> {
/**
* 分页查询执行成员变更历史,按 actionTime DESC, id DESC 排序。
* 支持按 actionType[] / userId / 时间范围筛选。
*/
default PageResult<ExecutionMemberLogDO> selectPageByExecutionId(Long executionId,
ExecutionMemberLogPageReqVO reqVO) {
LambdaQueryWrapperX<ExecutionMemberLogDO> queryWrapper = new LambdaQueryWrapperX<ExecutionMemberLogDO>()
.eq(ExecutionMemberLogDO::getExecutionId, executionId)
.inIfPresent(ExecutionMemberLogDO::getActionType, reqVO.getActionTypes())
.eqIfPresent(ExecutionMemberLogDO::getUserId, reqVO.getUserId())
.geIfPresent(ExecutionMemberLogDO::getActionTime, reqVO.getStartTime())
.leIfPresent(ExecutionMemberLogDO::getActionTime, reqVO.getEndTime())
.orderByDesc(ExecutionMemberLogDO::getActionTime)
.orderByDesc(ExecutionMemberLogDO::getId);
return selectPage(reqVO, queryWrapper);
}
}

View File

@@ -0,0 +1,51 @@
package com.njcn.rdms.module.project.dal.mysql.project.execution;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionMemberDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ExecutionMemberMapper extends BaseMapperX<ExecutionMemberDO> {
default List<ExecutionMemberDO> selectListByExecutionId(Long executionId) {
return selectList(new LambdaQueryWrapperX<ExecutionMemberDO>()
.eq(ExecutionMemberDO::getExecutionId, executionId)
.orderByAsc(ExecutionMemberDO::getRemovedAt)
.orderByAsc(ExecutionMemberDO::getJoinedAt)
.orderByAsc(ExecutionMemberDO::getId));
}
/**
* 仅返当前活跃成员removed_at IS NULL。B 模型下同 userId 至多一段未失效。
*/
default List<ExecutionMemberDO> selectActiveListByExecutionId(Long executionId) {
return selectList(new LambdaQueryWrapperX<ExecutionMemberDO>()
.eq(ExecutionMemberDO::getExecutionId, executionId)
.isNull(ExecutionMemberDO::getRemovedAt)
.orderByAsc(ExecutionMemberDO::getJoinedAt)
.orderByAsc(ExecutionMemberDO::getId));
}
default ExecutionMemberDO selectByExecutionIdAndUserId(Long executionId, Long userId) {
return selectOne(new LambdaQueryWrapperX<ExecutionMemberDO>()
.eq(ExecutionMemberDO::getExecutionId, executionId)
.eq(ExecutionMemberDO::getUserId, userId));
}
default ExecutionMemberDO selectByIdAndExecutionId(Long id, Long executionId) {
return selectOne(new LambdaQueryWrapperX<ExecutionMemberDO>()
.eq(ExecutionMemberDO::getId, id)
.eq(ExecutionMemberDO::getExecutionId, executionId));
}
default ExecutionMemberDO selectActiveByExecutionIdAndUserId(Long executionId, Long userId) {
return selectOne(new LambdaQueryWrapperX<ExecutionMemberDO>()
.eq(ExecutionMemberDO::getExecutionId, executionId)
.eq(ExecutionMemberDO::getUserId, userId)
.isNull(ExecutionMemberDO::getRemovedAt));
}
}

View File

@@ -0,0 +1,77 @@
package com.njcn.rdms.module.project.dal.mysql.project.execution;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.util.StringUtils;
import java.util.List;
@Mapper
public interface ProjectExecutionMapper extends BaseMapperX<ProjectExecutionDO> {
default ProjectExecutionDO selectByProjectIdAndId(Long projectId, Long executionId) {
return selectOne(new LambdaQueryWrapperX<ProjectExecutionDO>()
.eq(ProjectExecutionDO::getProjectId, projectId)
.eq(ProjectExecutionDO::getId, executionId));
}
default ProjectExecutionDO selectByProjectIdAndName(Long projectId, String executionName) {
return selectOne(new LambdaQueryWrapperX<ProjectExecutionDO>()
.eq(ProjectExecutionDO::getProjectId, projectId)
.eq(ProjectExecutionDO::getExecutionName, executionName));
}
default PageResult<ProjectExecutionDO> selectPageByProjectId(Long projectId, ProjectExecutionPageReqVO reqVO) {
LambdaQueryWrapperX<ProjectExecutionDO> queryWrapper = new LambdaQueryWrapperX<ProjectExecutionDO>()
.eq(ProjectExecutionDO::getProjectId, projectId)
.eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType())
.eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId())
.eqIfPresent(ProjectExecutionDO::getStatusCode, reqVO.getStatusCode())
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime())
.orderByDesc(BaseDO::getUpdateTime)
.orderByDesc(ProjectExecutionDO::getId);
if (StringUtils.hasText(reqVO.getKeyword())) {
queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword()));
}
return selectPage(reqVO, queryWrapper);
}
default Integer countNonTerminalByProjectIdAndOwnerId(Long projectId, Long ownerId, List<String> terminalStatusCodes) {
LambdaQueryWrapperX<ProjectExecutionDO> queryWrapper = new LambdaQueryWrapperX<ProjectExecutionDO>()
.eq(ProjectExecutionDO::getProjectId, projectId)
.eq(ProjectExecutionDO::getOwnerId, ownerId);
if (terminalStatusCodes != null && !terminalStatusCodes.isEmpty()) {
queryWrapper.notIn(ProjectExecutionDO::getStatusCode, terminalStatusCodes);
}
return Math.toIntExact(selectCount(queryWrapper));
}
default Integer countByProjectIdAndStatusCode(Long projectId, ProjectExecutionStatusBoardReqVO reqVO, String statusCode) {
LambdaQueryWrapperX<ProjectExecutionDO> queryWrapper = new LambdaQueryWrapperX<ProjectExecutionDO>()
.eq(ProjectExecutionDO::getProjectId, projectId)
.eq(ProjectExecutionDO::getStatusCode, statusCode)
.eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType())
.eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId())
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
if (StringUtils.hasText(reqVO.getKeyword())) {
queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword()));
}
return Math.toIntExact(selectCount(queryWrapper));
}
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
ProjectExecutionDO update = new ProjectExecutionDO();
update.setStatusCode(toStatus);
update.setLastStatusReason(lastStatusReason);
return update(update, new LambdaQueryWrapperX<ProjectExecutionDO>()
.eq(ProjectExecutionDO::getId, id)
.eq(ProjectExecutionDO::getStatusCode, fromStatus));
}
}

View File

@@ -0,0 +1,25 @@
package com.njcn.rdms.module.project.dal.mysql.project.execution;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionStatusLogDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface ProjectExecutionStatusLogMapper extends BaseMapperX<ProjectExecutionStatusLogDO> {
default List<ProjectExecutionStatusLogDO> selectListByExecutionId(Long executionId, String actionType,
LocalDateTime[] operateTime) {
return selectList(new LambdaQueryWrapperX<ProjectExecutionStatusLogDO>()
.eq(ProjectExecutionStatusLogDO::getExecutionId, executionId)
.eqIfPresent(ProjectExecutionStatusLogDO::getActionType, actionType)
.betweenIfPresent(BaseDO::getCreateTime, operateTime)
.orderByDesc(BaseDO::getCreateTime)
.orderByDesc(ProjectExecutionStatusLogDO::getId));
}
}

View File

@@ -0,0 +1,127 @@
package com.njcn.rdms.module.project.dal.mysql.project.task;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
@Mapper
public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
default ProjectTaskDO selectByProjectIdAndExecutionIdAndId(Long projectId, Long executionId, Long taskId) {
return selectOne(new LambdaQueryWrapperX<ProjectTaskDO>()
.eq(ProjectTaskDO::getProjectId, projectId)
.eq(ProjectTaskDO::getExecutionId, executionId)
.eq(ProjectTaskDO::getId, taskId));
}
default PageResult<ProjectTaskDO> selectPageByExecutionId(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) {
LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<>();
queryWrapper.eq(ProjectTaskDO::getProjectId, projectId);
queryWrapper.eq(ProjectTaskDO::getExecutionId, executionId);
queryWrapper.eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId());
queryWrapper.eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId());
queryWrapper.eqIfPresent(ProjectTaskDO::getStatusCode, reqVO.getStatusCode());
queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
queryWrapper.orderByAsc(ProjectTaskDO::getParentTaskId);
queryWrapper.orderByDesc(BaseDO::getUpdateTime);
queryWrapper.orderByDesc(ProjectTaskDO::getId);
if (StringUtils.hasText(reqVO.getKeyword())) {
queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword()));
}
return selectPage(reqVO, queryWrapper);
}
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
ProjectTaskDO update = new ProjectTaskDO();
update.setStatusCode(toStatus);
update.setLastStatusReason(lastStatusReason);
return update(update, new LambdaQueryWrapperX<ProjectTaskDO>()
.eq(ProjectTaskDO::getId, id)
.eq(ProjectTaskDO::getStatusCode, fromStatus));
}
/**
* 仅更新实际开始/结束日期。null 字段依据全局 FieldStrategy 不会被覆盖。
*/
default int updateActualDatesById(Long id, LocalDate actualStartDate, LocalDate actualEndDate) {
if (actualStartDate == null && actualEndDate == null) {
return 0;
}
ProjectTaskDO update = new ProjectTaskDO();
update.setActualStartDate(actualStartDate);
update.setActualEndDate(actualEndDate);
return update(update, new LambdaQueryWrapperX<ProjectTaskDO>()
.eq(ProjectTaskDO::getId, id));
}
default Integer countChildrenNotInStatus(Long parentTaskId, List<String> terminalStatusCodes) {
LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<ProjectTaskDO>()
.eq(ProjectTaskDO::getParentTaskId, parentTaskId);
if (terminalStatusCodes != null && !terminalStatusCodes.isEmpty()) {
queryWrapper.notIn(ProjectTaskDO::getStatusCode, terminalStatusCodes);
}
return Math.toIntExact(selectCount(queryWrapper));
}
/**
* 数指定父任务下的直接子任务(不区分状态)。用于"是否叶子任务"判定与进度汇总。
*/
default int countChildrenByParentTaskId(Long parentTaskId) {
if (parentTaskId == null) {
return 0;
}
return Math.toIntExact(selectCount(new LambdaQueryWrapperX<ProjectTaskDO>()
.eq(ProjectTaskDO::getParentTaskId, parentTaskId)));
}
/**
* 取直接子任务的 progressRate 列表(用于父任务进度简单平均汇总)。
* 只读取必要字段,避免拉回整行;逻辑删除的子任务由 BaseMapper 自动过滤。
*/
default List<ProjectTaskDO> selectChildrenProgressByParentTaskId(Long parentTaskId) {
if (parentTaskId == null) {
return Collections.emptyList();
}
return selectList(new LambdaQueryWrapperX<ProjectTaskDO>()
.select(ProjectTaskDO::getId, ProjectTaskDO::getProgressRate)
.eq(ProjectTaskDO::getParentTaskId, parentTaskId));
}
/**
* 仅更新单个任务的 progressRate不动其他字段避免污染 lastStatusReason 等)。
*/
default int updateProgressRateById(Long id, BigDecimal progressRate) {
ProjectTaskDO update = new ProjectTaskDO();
update.setProgressRate(progressRate);
return update(update, new LambdaQueryWrapperX<ProjectTaskDO>()
.eq(ProjectTaskDO::getId, id));
}
default Integer countByProjectIdAndExecutionIdAndStatusCode(Long projectId, Long executionId,
ProjectTaskStatusBoardReqVO reqVO,
String statusCode) {
LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<ProjectTaskDO>()
.eq(ProjectTaskDO::getProjectId, projectId)
.eq(ProjectTaskDO::getExecutionId, executionId)
.eq(ProjectTaskDO::getStatusCode, statusCode)
.eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId())
.eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId())
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
if (StringUtils.hasText(reqVO.getKeyword())) {
queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword()));
}
return Math.toIntExact(selectCount(queryWrapper));
}
}

View File

@@ -0,0 +1,9 @@
package com.njcn.rdms.module.project.dal.mysql.project.task;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskStatusLogDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProjectTaskStatusLogMapper extends BaseMapperX<ProjectTaskStatusLogDO> {
}

View File

@@ -0,0 +1,29 @@
package com.njcn.rdms.module.project.dal.mysql.project.task;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeLogPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeLogDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TaskAssigneeLogMapper extends BaseMapperX<TaskAssigneeLogDO> {
/**
* 分页查询协办人变更历史,按 actionTime DESC, id DESC 排序。
* 支持按 actionType[] / userId / 时间范围筛选。
*/
default PageResult<TaskAssigneeLogDO> selectPageByTaskId(Long taskId, TaskAssigneeLogPageReqVO reqVO) {
LambdaQueryWrapperX<TaskAssigneeLogDO> queryWrapper = new LambdaQueryWrapperX<TaskAssigneeLogDO>()
.eq(TaskAssigneeLogDO::getTaskId, taskId)
.inIfPresent(TaskAssigneeLogDO::getActionType, reqVO.getActionTypes())
.eqIfPresent(TaskAssigneeLogDO::getUserId, reqVO.getUserId())
.geIfPresent(TaskAssigneeLogDO::getActionTime, reqVO.getStartTime())
.leIfPresent(TaskAssigneeLogDO::getActionTime, reqVO.getEndTime())
.orderByDesc(TaskAssigneeLogDO::getActionTime)
.orderByDesc(TaskAssigneeLogDO::getId);
return selectPage(reqVO, queryWrapper);
}
}

View File

@@ -0,0 +1,60 @@
package com.njcn.rdms.module.project.dal.mysql.project.task;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Mapper
public interface TaskAssigneeMapper extends BaseMapperX<TaskAssigneeDO> {
/**
* 查同一任务下指定 userId 当前活跃的协办段removed_at IS NULL。失效段不会命中。
*/
default TaskAssigneeDO selectActiveByTaskIdAndUserId(Long taskId, Long userId) {
return selectOne(new LambdaQueryWrapperX<TaskAssigneeDO>()
.eq(TaskAssigneeDO::getTaskId, taskId)
.eq(TaskAssigneeDO::getUserId, userId)
.isNull(TaskAssigneeDO::getRemovedAt));
}
/**
* 查指定任务下所有当前活跃协办,按加入时间正序。
*/
default List<TaskAssigneeDO> selectActiveListByTaskId(Long taskId) {
return selectList(new LambdaQueryWrapperX<TaskAssigneeDO>()
.eq(TaskAssigneeDO::getTaskId, taskId)
.isNull(TaskAssigneeDO::getRemovedAt)
.orderByAsc(TaskAssigneeDO::getJoinedAt)
.orderByAsc(TaskAssigneeDO::getId));
}
/**
* 批量查多个任务的活跃协办(分页装配 N+1 优化)。
*/
default List<TaskAssigneeDO> selectActiveListByTaskIds(Collection<Long> taskIds) {
if (taskIds == null || taskIds.isEmpty()) {
return Collections.emptyList();
}
return selectList(new LambdaQueryWrapperX<TaskAssigneeDO>()
.in(TaskAssigneeDO::getTaskId, taskIds)
.isNull(TaskAssigneeDO::getRemovedAt)
.orderByAsc(TaskAssigneeDO::getTaskId)
.orderByAsc(TaskAssigneeDO::getJoinedAt)
.orderByAsc(TaskAssigneeDO::getId));
}
/**
* 按主键 + 任务 ID 双键查返回的记录可能已失效removed_at != null由调用方判断。
*/
default TaskAssigneeDO selectByIdAndTaskId(Long id, Long taskId) {
return selectOne(new LambdaQueryWrapperX<TaskAssigneeDO>()
.eq(TaskAssigneeDO::getId, id)
.eq(TaskAssigneeDO::getTaskId, taskId));
}
}

View File

@@ -0,0 +1,78 @@
package com.njcn.rdms.module.project.dal.mysql.project.task;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskWorklogDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@Mapper
public interface TaskWorklogMapper extends BaseMapperX<TaskWorklogDO> {
/**
* 按主键 + 任务 ID 双键查;逻辑删除的记录不会命中(依赖 BaseMapper 自带的 deleted=0 过滤)。
* 用于 update / delete 前的归属校验。
*/
default TaskWorklogDO selectByIdAndTaskId(Long id, Long taskId) {
return selectOne(new LambdaQueryWrapperX<TaskWorklogDO>()
.eq(TaskWorklogDO::getId, id)
.eq(TaskWorklogDO::getTaskId, taskId));
}
/**
* 任务工时分页:按 workDate DESC, id DESC支持按填报人 / 日期区间筛选。
*/
default PageResult<TaskWorklogDO> selectPageByTaskId(Long taskId, TaskWorklogPageReqVO reqVO) {
LambdaQueryWrapperX<TaskWorklogDO> queryWrapper = new LambdaQueryWrapperX<TaskWorklogDO>()
.eq(TaskWorklogDO::getTaskId, taskId)
.eqIfPresent(TaskWorklogDO::getUserId, reqVO.getUserId())
.geIfPresent(TaskWorklogDO::getWorkDate, reqVO.getStartDate())
.leIfPresent(TaskWorklogDO::getWorkDate, reqVO.getEndDate())
.orderByDesc(TaskWorklogDO::getWorkDate)
.orderByDesc(TaskWorklogDO::getId);
return selectPage(reqVO, queryWrapper);
}
/**
* 单任务工时汇总(分钟)。无记录时返回 0逻辑删除的记录不参与汇总。
*/
@Select("""
SELECT COALESCE(SUM(duration_minutes), 0)
FROM rdms_task_worklog
WHERE deleted = b'0' AND task_id = #{taskId}
""")
Long sumDurationByTaskId(@Param("taskId") Long taskId);
/**
* 批量任务工时汇总(分钟),返回 [{taskId, total}]。用于详情/分页装配避免 N+1。
*/
@Select("""
<script>
SELECT task_id AS taskId, COALESCE(SUM(duration_minutes), 0) AS total
FROM rdms_task_worklog
WHERE deleted = b'0' AND task_id IN
<foreach collection="taskIds" item="id" open="(" separator="," close=")">#{id}</foreach>
GROUP BY task_id
</script>
""")
List<Map<String, Object>> sumDurationGroupByTaskIds(@Param("taskIds") Collection<Long> taskIds);
/**
* 是否存在任意工时记录。叶子转父校验用(拆子任务前必须删完工时)。
*/
default boolean existsByTaskId(Long taskId) {
if (taskId == null) {
return false;
}
return selectCount(new LambdaQueryWrapperX<TaskWorklogDO>()
.eq(TaskWorklogDO::getTaskId, taskId)) > 0;
}
}

View File

@@ -6,6 +6,7 @@ import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.stream.Collectors;
@Mapper
public interface ObjectStatusModelMapper extends BaseMapperX<ObjectStatusModelDO> {
@@ -23,10 +24,37 @@ public interface ObjectStatusModelMapper extends BaseMapperX<ObjectStatusModelDO
.eq(ObjectStatusModelDO::getStatus, 0));
}
default ObjectStatusModelDO selectInitialByObjectTypeEnabled(String objectType) {
return selectOne(new LambdaQueryWrapperX<ObjectStatusModelDO>()
.eq(ObjectStatusModelDO::getObjectType, objectType)
.eq(ObjectStatusModelDO::getInitialFlag, true)
.eq(ObjectStatusModelDO::getStatus, 0));
}
default List<ObjectStatusModelDO> selectListByObjectType(String objectType) {
return selectList(new LambdaQueryWrapperX<ObjectStatusModelDO>()
.eq(ObjectStatusModelDO::getObjectType, objectType)
.orderByAsc(ObjectStatusModelDO::getSort));
}
default List<ObjectStatusModelDO> selectListByObjectTypeEnabled(String objectType) {
return selectList(new LambdaQueryWrapperX<ObjectStatusModelDO>()
.eq(ObjectStatusModelDO::getObjectType, objectType)
.eq(ObjectStatusModelDO::getStatus, 0)
.orderByAsc(ObjectStatusModelDO::getSort));
}
/**
* 查询某对象类型下所有已启用的终态状态码。
*/
default List<String> selectTerminalStatusCodesByObjectTypeEnabled(String objectType) {
return selectList(new LambdaQueryWrapperX<ObjectStatusModelDO>()
.eq(ObjectStatusModelDO::getObjectType, objectType)
.eq(ObjectStatusModelDO::getStatus, 0)
.eq(ObjectStatusModelDO::getTerminalFlag, true))
.stream()
.map(ObjectStatusModelDO::getStatusCode)
.collect(Collectors.toList());
}
}

View File

@@ -1,5 +1,6 @@
package com.njcn.rdms.module.project.framework.rpc.config;
import com.njcn.rdms.module.system.api.dict.DictDataApi;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import org.springframework.cloud.openfeign.EnableFeignClients;
@@ -9,6 +10,6 @@ import org.springframework.context.annotation.Configuration;
* Project 模块的 RPC 配置
*/
@Configuration(value = "projectRpcConfiguration", proxyBeanMethods = false)
@EnableFeignClients(clients = {AdminUserApi.class, ObjectPermissionApi.class})
@EnableFeignClients(clients = {AdminUserApi.class, ObjectPermissionApi.class, DictDataApi.class})
public class RpcConfiguration {
}

View File

@@ -1,11 +1,12 @@
package com.njcn.rdms.module.project.framework.security.service;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -24,9 +25,6 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
@Service
public class ProductObjectPermissionService implements ObjectPermissionService {
private static final String PRODUCT_OBJECT_TYPE = "product";
private static final String ROLE_SCOPE_OBJECT = PermissionScopeTypeEnum.OBJECT.getScopeType();
@Resource
private UserObjectRoleMapper userObjectRoleMapper;
@Resource
@@ -34,7 +32,7 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
@Override
public String getObjectType() {
return PRODUCT_OBJECT_TYPE;
return ProductObjectConstants.OBJECT_TYPE;
}
@Override
@@ -44,7 +42,7 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
}
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
UserObjectRoleDO currentMember = userObjectRoleMapper
.selectActiveByObjectAndUserId(PRODUCT_OBJECT_TYPE, objectId, loginUserId);
.selectActiveByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, objectId, loginUserId);
if (currentMember == null) {
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED,
buildDeniedPermission(permission, memberOnly));
@@ -60,7 +58,8 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
private Set<String> getRolePermissions(Long roleId) {
Set<String> permissions = objectPermissionApi
.getObjectRolePermissions(roleId, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
.getObjectRolePermissions(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT,
ProductObjectConstants.OBJECT_TYPE)
.getCheckedData();
if (permissions == null || permissions.isEmpty()) {
return Collections.emptySet();

View File

@@ -0,0 +1,83 @@
package com.njcn.rdms.module.project.framework.security.service;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
/**
* 项目对象权限服务。
*/
@Service
public class ProjectObjectPermissionService implements ObjectPermissionService {
@Resource
private UserObjectRoleMapper userObjectRoleMapper;
@Resource
private ObjectPermissionApi objectPermissionApi;
@Override
public String getObjectType() {
return ProjectObjectConstants.OBJECT_TYPE;
}
@Override
public void checkPermission(Long objectId, String permission, boolean memberOnly) {
if (objectId == null) {
throw invalidParamException("对象编号不能为空");
}
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
UserObjectRoleDO currentMember = userObjectRoleMapper
.selectActiveByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, loginUserId);
if (currentMember == null) {
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED,
buildDeniedPermission(permission, memberOnly));
}
if (memberOnly) {
return;
}
String normalizedPermission = normalizePermission(permission);
if (!getRolePermissions(currentMember.getRoleId()).contains(normalizedPermission)) {
throw exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED, normalizedPermission);
}
}
private Set<String> getRolePermissions(Long roleId) {
Set<String> permissions = objectPermissionApi
.getObjectRolePermissions(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
.getCheckedData();
if (permissions == null || permissions.isEmpty()) {
return Collections.emptySet();
}
return permissions.stream()
.filter(StringUtils::hasText)
.map(String::trim)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
private String normalizePermission(String permission) {
if (!StringUtils.hasText(permission)) {
throw invalidParamException("对象权限码不能为空");
}
return permission.trim();
}
private String buildDeniedPermission(String permission, boolean memberOnly) {
return memberOnly ? "member" : normalizePermission(permission);
}
}

View File

@@ -6,6 +6,8 @@ import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.PageUtils;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityPageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityRespVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
@@ -37,10 +39,8 @@ import java.util.stream.Collectors;
@Service
public class ProductActivityQueryService {
private static final String PRODUCT_OBJECT_TYPE = ObjectActivityConstants.PRODUCT_BIZ_TYPE;
private static final String MEMBER_BIZ_TYPE = ObjectActivityConstants.MEMBER_BIZ_TYPE;
private static final String ACTIVITY_ROLE_NAME_CACHE = "project_activity_role_name#10m";
private static final String ROLE_SCOPE_OBJECT = "object";
@Resource
private ProductStatusLogMapper productStatusLogMapper;
@@ -60,7 +60,8 @@ public class ProductActivityQueryService {
.forEach(log -> items.add(new ActivityItem(log.getId(), log.getCreateTime(), toStatusActivity(log))));
}
if (includeType(reqVO.getActivityType(), ObjectActivityConstants.ACTIVITY_TYPE_PRODUCT)) {
bizAuditLogMapper.selectListByBiz(PRODUCT_OBJECT_TYPE, productId, reqVO.getActionType(), reqVO.getOperateTime())
bizAuditLogMapper.selectListByBiz(ProductObjectConstants.OBJECT_TYPE, productId,
reqVO.getActionType(), reqVO.getOperateTime())
.forEach(log -> items.add(new ActivityItem(log.getId(), log.getCreateTime(), toProductActivity(log))));
}
if (includeType(reqVO.getActivityType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
@@ -96,7 +97,7 @@ public class ProductActivityQueryService {
}
Map<Long, UserObjectRoleDO> memberMap = userObjectRoleMapper
.selectListByIdsAndObject(memberIds, PRODUCT_OBJECT_TYPE, productId)
.selectListByIdsAndObject(memberIds, ProductObjectConstants.OBJECT_TYPE, productId)
.stream()
.collect(Collectors.toMap(UserObjectRoleDO::getId, Function.identity()));
memberLogs.stream()
@@ -166,7 +167,8 @@ public class ProductActivityQueryService {
return roleNameMap;
}
Map<Long, ObjectRoleRespDTO> roleMap = objectPermissionApi == null ? Map.of()
: objectPermissionApi.getObjectRoleMap(missIds, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE);
: objectPermissionApi.getObjectRoleMap(missIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT,
ProductObjectConstants.OBJECT_TYPE);
if (roleMap == null || roleMap.isEmpty()) {
return roleNameMap;
}

View File

@@ -6,6 +6,8 @@ import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.PageUtils;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelinePageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelineRespVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
@@ -44,8 +46,6 @@ public class ProductActivityTimelineQueryService {
*/
private static final String TIMELINE_USER_NICKNAME_CACHE = "project_timeline_user_nickname#10m";
private static final String TIMELINE_ROLE_NAME_CACHE = "project_timeline_role_name#10m";
private static final String ROLE_SCOPE_OBJECT = "object";
private static final String PRODUCT_OBJECT_TYPE = ObjectActivityConstants.PRODUCT_BIZ_TYPE;
@Resource
private ProductStatusLogMapper productStatusLogMapper;
@@ -85,10 +85,7 @@ public class ProductActivityTimelineQueryService {
if (!includeType(activityType, ObjectActivityConstants.ACTIVITY_TYPE_STATUS)) {
return;
}
List<String> statusActions = limitActions(actionTypes, ObjectActivityConstants.STATUS_ACTION_TYPES);
if (shouldSkipByIntersection(actionTypes, statusActions)) {
return;
}
List<String> statusActions = actionTypes == null || actionTypes.isEmpty() ? null : actionTypes;
productStatusLogMapper.selectListByProductIdAndActions(productId, statusActions, timeRange[0], timeRange[1])
.forEach(log -> items.add(new ActivityItem(log.getId(), log.getCreateTime(), toStatusTimeline(log))));
}
@@ -103,7 +100,7 @@ public class ProductActivityTimelineQueryService {
return;
}
List<BizAuditLogDO> productLogs = bizAuditLogMapper.selectListByBizAndActions(
ObjectActivityConstants.PRODUCT_BIZ_TYPE, productId, productActions, timeRange[0], timeRange[1]);
ProductObjectConstants.OBJECT_TYPE, productId, productActions, timeRange[0], timeRange[1]);
Set<CreateSignature> createSignatures = buildCreateSignatures(productLogs);
for (BizAuditLogDO log : productLogs) {
if (ObjectActivityConstants.isStatusAction(log.getActionType())) {
@@ -205,7 +202,7 @@ public class ProductActivityTimelineQueryService {
}
Map<Long, UserObjectRoleDO> memberMap = new LinkedHashMap<>();
for (UserObjectRoleDO member : userObjectRoleMapper.selectListByIdsAndObject(
memberIds, ObjectActivityConstants.PRODUCT_BIZ_TYPE, productId)) {
memberIds, ProductObjectConstants.OBJECT_TYPE, productId)) {
memberMap.put(member.getId(), member);
}
return memberMap;
@@ -213,7 +210,7 @@ public class ProductActivityTimelineQueryService {
private Set<CreateSignature> loadCreateSignatures(Long productId, LocalDateTime[] timeRange) {
List<BizAuditLogDO> productLogs = bizAuditLogMapper.selectListByBizAndActions(
ObjectActivityConstants.PRODUCT_BIZ_TYPE, productId,
ProductObjectConstants.OBJECT_TYPE, productId,
ObjectActivityConstants.PRODUCT_TIMELINE_ACTION_TYPES, timeRange[0], timeRange[1]);
return buildCreateSignatures(productLogs);
}
@@ -402,7 +399,8 @@ public class ProductActivityTimelineQueryService {
return roleNameMap;
}
Map<Long, ObjectRoleRespDTO> roleMap = objectPermissionApi == null ? Map.of()
: objectPermissionApi.getObjectRoleMap(missIds, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE);
: objectPermissionApi.getObjectRoleMap(missIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT,
ProductObjectConstants.OBJECT_TYPE);
if (roleMap == null || roleMap.isEmpty()) {
return roleNameMap;
}

View File

@@ -3,6 +3,8 @@ package com.njcn.rdms.module.project.service.product;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
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;
@@ -11,9 +13,11 @@ import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPer
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
@@ -42,16 +46,6 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
@Service
public class ProductMemberServiceImpl implements ProductMemberService {
private static final String PRODUCT_OBJECT_TYPE = "product";
private static final String ROLE_SCOPE_OBJECT = "object";
private static final String PRODUCT_QUERY_PERMISSION = "project:product:query";
private static final String PRODUCT_UPDATE_PERMISSION = "project:product:update";
private static final Integer MEMBER_STATUS_ACTIVE = 0;
private static final Integer MEMBER_STATUS_INACTIVE = 1;
private static final String PRODUCT_MANAGER_ROLE_CODE = "product_manager";
@Resource
private ProductMapper productMapper;
@Resource
@@ -61,14 +55,14 @@ public class ProductMemberServiceImpl implements ProductMemberService {
@Resource
private BizAuditLogMapper bizAuditLogMapper;
@Resource
private ObjectStatusModelMapper objectStatusModelMapper;
@Resource
private AdminUserApi adminUserApi;
@Override
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_QUERY_PERMISSION)
public List<ProductMemberRespVO> getProductMemberList(Long productId) {
ProductDO product = validateProductExists(productId);
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(PRODUCT_OBJECT_TYPE, productId);
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProductObjectConstants.OBJECT_TYPE, productId);
Map<Long, ObjectRoleRespDTO> roleMap = getRoleMap(members.stream().map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet()));
Map<Long, AdminUserRespDTO> userMap = getUserMap(members.stream().map(UserObjectRoleDO::getUserId).collect(Collectors.toSet()));
return members.stream().map(member -> {
@@ -82,7 +76,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
respVO.setRoleName(role == null ? null : role.getName());
respVO.setRoleCode(role == null ? null : role.getCode());
respVO.setManagerFlag(Objects.equals(member.getUserId(), product.getManagerUserId())
&& Objects.equals(member.getStatus(), MEMBER_STATUS_ACTIVE));
&& Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE));
respVO.setStatus(member.getStatus());
respVO.setJoinedTime(member.getJoinedTime());
respVO.setLeftTime(member.getLeftTime());
@@ -93,14 +87,14 @@ public class ProductMemberServiceImpl implements ProductMemberService {
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_UPDATE_PERMISSION)
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
permission = ProductObjectConstants.PERMISSION_UPDATE)
public Long createProductMember(Long productId, ProductMemberSaveReqVO reqVO) {
ProductDO product = validateProductExists(productId);
ProductDO product = validateProductEditable(productId);
ObjectRoleRespDTO targetRole = validateProductRole(reqVO.getRoleId());
UserObjectRoleDO existingMember = userObjectRoleMapper
.selectByObjectAndUserId(PRODUCT_OBJECT_TYPE, productId, reqVO.getUserId());
if (existingMember != null && Objects.equals(existingMember.getStatus(), MEMBER_STATUS_ACTIVE)) {
.selectByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, productId, reqVO.getUserId());
if (existingMember != null && Objects.equals(existingMember.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_ALREADY_EXISTS);
}
@@ -110,10 +104,10 @@ public class ProductMemberServiceImpl implements ProductMemberService {
if (existingMember == null) {
member = new UserObjectRoleDO();
member.setUserId(reqVO.getUserId());
member.setObjectType(PRODUCT_OBJECT_TYPE);
member.setObjectType(ProductObjectConstants.OBJECT_TYPE);
member.setObjectId(productId);
member.setRoleId(targetRole.getId());
member.setStatus(MEMBER_STATUS_ACTIVE);
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
member.setJoinedTime(now);
member.setLeftTime(null);
member.setRemark(normalizeNullableText(reqVO.getRemark()));
@@ -122,7 +116,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
before = cloneMember(existingMember);
member = existingMember;
member.setRoleId(targetRole.getId());
member.setStatus(MEMBER_STATUS_ACTIVE);
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
member.setJoinedTime(now);
member.setLeftTime(null);
member.setRemark(normalizeNullableText(reqVO.getRemark()));
@@ -138,12 +132,12 @@ public class ProductMemberServiceImpl implements ProductMemberService {
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_UPDATE_PERMISSION)
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
permission = ProductObjectConstants.PERMISSION_UPDATE)
public void updateProductMember(Long productId, Long memberId, ProductMemberUpdateReqVO reqVO) {
ProductDO product = validateProductExists(productId);
ProductDO product = validateProductEditable(productId);
UserObjectRoleDO member = validateMemberExists(productId, memberId);
if (!Objects.equals(member.getStatus(), MEMBER_STATUS_ACTIVE)) {
if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_NOT_ACTIVE);
}
@@ -170,12 +164,12 @@ public class ProductMemberServiceImpl implements ProductMemberService {
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_UPDATE_PERMISSION)
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
permission = ProductObjectConstants.PERMISSION_UPDATE)
public void inactiveProductMember(Long productId, Long memberId, ProductMemberInactiveReqVO reqVO) {
ProductDO product = validateProductExists(productId);
ProductDO product = validateProductEditable(productId);
UserObjectRoleDO member = validateMemberExists(productId, memberId);
if (!Objects.equals(member.getStatus(), MEMBER_STATUS_ACTIVE)) {
if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_NOT_ACTIVE);
}
if (Objects.equals(member.getUserId(), product.getManagerUserId())) {
@@ -183,7 +177,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
}
UserObjectRoleDO before = cloneMember(member);
member.setStatus(MEMBER_STATUS_INACTIVE);
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_INACTIVE);
member.setLeftTime(LocalDateTime.now());
userObjectRoleMapper.updateById(member);
@@ -202,8 +196,21 @@ public class ProductMemberServiceImpl implements ProductMemberService {
return product;
}
private ProductDO validateProductEditable(Long productId) {
ProductDO product = validateProductExists(productId);
ObjectStatusModelDO statusModel = objectStatusModelMapper
.selectByObjectTypeAndStatusCodeEnabled(ProductObjectConstants.OBJECT_TYPE, product.getStatusCode());
if (statusModel == null) {
throw exception(ErrorCodeConstants.PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
}
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
throw exception(ErrorCodeConstants.PRODUCT_STATUS_NOT_ALLOW_EDIT);
}
return product;
}
private UserObjectRoleDO validateMemberExists(Long productId, Long memberId) {
UserObjectRoleDO member = userObjectRoleMapper.selectByIdAndObject(memberId, PRODUCT_OBJECT_TYPE, productId);
UserObjectRoleDO member = userObjectRoleMapper.selectByIdAndObject(memberId, ProductObjectConstants.OBJECT_TYPE, productId);
if (member == null) {
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_NOT_EXISTS);
}
@@ -212,7 +219,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
private ObjectRoleRespDTO validateProductRole(Long roleId) {
ObjectRoleRespDTO role = objectPermissionApi
.getObjectRoleById(roleId, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
.getObjectRoleById(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProductObjectConstants.OBJECT_TYPE)
.getCheckedData();
if (role == null) {
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_ROLE_INVALID);
@@ -262,7 +269,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
private void transferPreviousManager(Long productId, Long previousManagerUserId, Long previousManagerRoleId, String reason) {
UserObjectRoleDO existingMember = userObjectRoleMapper
.selectByObjectAndUserId(PRODUCT_OBJECT_TYPE, productId, previousManagerUserId);
.selectByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, productId, previousManagerUserId);
UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember);
LocalDateTime now = LocalDateTime.now();
UserObjectRoleDO member;
@@ -270,10 +277,10 @@ public class ProductMemberServiceImpl implements ProductMemberService {
if (existingMember == null) {
member = new UserObjectRoleDO();
member.setUserId(previousManagerUserId);
member.setObjectType(PRODUCT_OBJECT_TYPE);
member.setObjectType(ProductObjectConstants.OBJECT_TYPE);
member.setObjectId(productId);
member.setRoleId(previousManagerRoleId);
member.setStatus(MEMBER_STATUS_ACTIVE);
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
member.setJoinedTime(now);
member.setLeftTime(null);
member.setRemark(null);
@@ -282,9 +289,9 @@ public class ProductMemberServiceImpl implements ProductMemberService {
} else {
member = existingMember;
member.setRoleId(previousManagerRoleId);
member.setStatus(MEMBER_STATUS_ACTIVE);
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
member.setLeftTime(null);
if (!Objects.equals(before.getStatus(), MEMBER_STATUS_ACTIVE)) {
if (!Objects.equals(before.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
member.setJoinedTime(now);
actionType = ObjectActivityConstants.MEMBER_ACTION_ADD;
} else {
@@ -296,7 +303,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
}
private boolean isManagerRole(ObjectRoleRespDTO role) {
return Objects.equals(PRODUCT_MANAGER_ROLE_CODE, role.getCode());
return Objects.equals(ProductObjectConstants.MANAGER_ROLE_CODE, role.getCode());
}
private Map<Long, ObjectRoleRespDTO> getRoleMap(Set<Long> roleIds) {
@@ -304,7 +311,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
return Collections.emptyMap();
}
List<ObjectRoleRespDTO> roles = objectPermissionApi
.getObjectRoleList(roleIds, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
.getObjectRoleList(roleIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProductObjectConstants.OBJECT_TYPE)
.getCheckedData();
if (roles == null || roles.isEmpty()) {
return Collections.emptyMap();
@@ -340,7 +347,7 @@ public class ProductMemberServiceImpl implements ProductMemberService {
return;
}
BizAuditLogDO auditLog = new BizAuditLogDO();
auditLog.setBizType(PRODUCT_OBJECT_TYPE);
auditLog.setBizType(ProductObjectConstants.OBJECT_TYPE);
auditLog.setBizId(productId);
auditLog.setActionType(ObjectActivityConstants.PRODUCT_ACTION_CHANGE_MANAGER);
auditLog.setFieldChanges(buildManagerFieldChanges(beforeManagerUserId, afterManagerUserId));

View File

@@ -3,6 +3,7 @@ package com.njcn.rdms.module.project.service.product;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
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.ProductSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
@@ -61,6 +62,13 @@ public interface ProductService {
*/
PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO);
/**
* 获取产品入口页概览统计
*
* @return 产品入口页概览统计
*/
ProductOverviewSummaryRespVO getProductOverviewSummary();
/**
* 变更产品状态
*

View File

@@ -6,18 +6,31 @@ import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextNavRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextProductRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRoleRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
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.ProductSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.*;
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
@@ -48,24 +61,6 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
@Service
public class ProductServiceImpl implements ProductService {
private static final String PRODUCT_OBJECT_TYPE = "product";
private static final String ROLE_SCOPE_OBJECT = PermissionScopeTypeEnum.OBJECT.getScopeType();
private static final String PRODUCT_MANAGER_ROLE_CODE = "product_manager";
private static final String PRODUCT_ACTIVE_STATUS = "active";
private static final String PRODUCT_PAUSED_STATUS = "paused";
private static final String PRODUCT_ARCHIVED_STATUS = "archived";
private static final String PRODUCT_ABANDONED_STATUS = "abandoned";
private static final Integer MEMBER_STATUS_ACTIVE = 0;
private static final String PRODUCT_CODE_PREFIX = "CNPD";
private static final String PRODUCT_QUERY_PERMISSION = "project:product:query";
private static final String PRODUCT_UPDATE_PERMISSION = "project:product:update";
private static final String PRODUCT_STATUS_PERMISSION = "project:product:status";
private static final String PRODUCT_DELETE_PERMISSION = "project:product:delete";
private static final String PRODUCT_DELETE_CONFIRM_TEXT = "DELETE";
@Resource
private ProductMapper productMapper;
@Resource
@@ -75,6 +70,8 @@ public class ProductServiceImpl implements ProductService {
@Resource
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
@Resource
private ObjectStatusModelMapper objectStatusModelMapper;
@Resource
private UserObjectRoleMapper userObjectRoleMapper;
@Resource
private ObjectPermissionApi objectPermissionApi;
@@ -92,7 +89,8 @@ public class ProductServiceImpl implements ProductService {
ProductDO product = new ProductDO();
product.setCode(generateProductCode(createReqVO.getCode()));
product.setDirectionCode(createReqVO.getDirectionCode());
product.setStatusCode(PRODUCT_ACTIVE_STATUS);
String initialStatus = getInitialStatusCode();
product.setStatusCode(initialStatus);
product.setName(createReqVO.getName().trim());
product.setManagerUserId(createReqVO.getManagerUserId());
product.setDescription(normalizeNullableText(createReqVO.getDescription()));
@@ -100,15 +98,15 @@ public class ProductServiceImpl implements ProductService {
initManagerMemberRelation(product);
initDefaultRequirementModule(product);
writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_CREATE, null, PRODUCT_ACTIVE_STATUS,
writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_CREATE, null, initialStatus,
buildProductFieldChanges(null, product), null);
return product.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#updateReqVO.id",
permission = PRODUCT_UPDATE_PERMISSION)
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#updateReqVO.id",
permission = ProductObjectConstants.PERMISSION_UPDATE)
public void updateProduct(ProductSaveReqVO updateReqVO) {
if (updateReqVO.getId() == null) {
throw invalidParamException("产品编号不能为空");
@@ -121,28 +119,25 @@ public class ProductServiceImpl implements ProductService {
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
permission = PRODUCT_UPDATE_PERMISSION)
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
permission = ProductObjectConstants.PERMISSION_UPDATE)
public void updateProductBaseInfo(Long productId, ProductSettingBaseInfoUpdateReqVO reqVO) {
ProductDO product = validateProductExists(productId);
applyProductBaseInfoUpdate(product, reqVO.getDirectionCode(), reqVO.getName(), reqVO.getDescription());
}
@Override
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#id",
permission = PRODUCT_QUERY_PERMISSION)
public ProductDO getProduct(Long id) {
return validateProductExists(id);
}
@Override
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#id", memberOnly = true)
public ProductContextRespVO getProductContext(Long id) {
ProductDO product = validateProductExists(id);
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
UserObjectRoleDO currentMember = userObjectRoleMapper
.selectActiveByObjectAndUserId(PRODUCT_OBJECT_TYPE, id, loginUserId);
.selectActiveByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, id, loginUserId);
ProductContextRespVO respVO = new ProductContextRespVO();
respVO.setCurrentProduct(buildCurrentProduct(product));
if (currentMember == null) {
@@ -152,7 +147,8 @@ public class ProductServiceImpl implements ProductService {
}
ObjectRolePermissionRespDTO permissionDetail = objectPermissionApi
.getObjectRolePermissionDetail(currentMember.getRoleId(), ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
.getObjectRolePermissionDetail(currentMember.getRoleId(), ObjectRoleConstants.ROLE_SCOPE_OBJECT,
ProductObjectConstants.OBJECT_TYPE)
.getCheckedData();
ObjectRoleRespDTO currentRole = permissionDetail == null ? null : permissionDetail.getCurrentRole();
List<ObjectMenuRespDTO> menus = permissionDetail == null || permissionDetail.getMenus() == null
@@ -169,10 +165,18 @@ public class ProductServiceImpl implements ProductService {
return productMapper.selectPage(pageReqVO);
}
@Override
public ProductOverviewSummaryRespVO getProductOverviewSummary() {
ProductOverviewSummaryRespVO respVO = new ProductOverviewSummaryRespVO();
respVO.setStatusCounts(buildProductStatusCounts(objectStatusModelMapper
.selectListByObjectType(ProductObjectConstants.OBJECT_TYPE), productMapper.selectStatusCountList()));
return respVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.id",
permission = PRODUCT_STATUS_PERMISSION)
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#reqVO.id",
permission = ProductObjectConstants.PERMISSION_STATUS)
public void changeProductStatus(ProductStatusActionReqVO reqVO) {
ProductDO product = validateProductExists(reqVO.getId());
String actionCode = reqVO.getActionCode().trim();
@@ -195,8 +199,8 @@ public class ProductServiceImpl implements ProductService {
@Override
@Transactional(rollbackFor = Exception.class)
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.id",
permission = PRODUCT_DELETE_PERMISSION)
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#reqVO.id",
permission = ProductObjectConstants.PERMISSION_DELETE)
public void deleteProduct(ProductDeleteReqVO reqVO) {
ProductDO product = validateProductExists(reqVO.getId());
validateDeleteConfirmText(reqVO.getConfirmText());
@@ -289,11 +293,11 @@ public class ProductServiceImpl implements ProductService {
@VisibleForTesting
void validateProductEditable(ProductDO product, String directionCode, String name) {
if (PRODUCT_ARCHIVED_STATUS.equals(product.getStatusCode())
|| PRODUCT_ABANDONED_STATUS.equals(product.getStatusCode())) {
ObjectStatusModelDO statusModel = validateEnabledStatusModel(product.getStatusCode());
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
throw exception(ErrorCodeConstants.PRODUCT_STATUS_NOT_ALLOW_EDIT);
}
if (!PRODUCT_PAUSED_STATUS.equals(product.getStatusCode())) {
if (!ProductObjectConstants.STATUS_PAUSED.equals(product.getStatusCode())) {
return;
}
if (!Objects.equals(product.getDirectionCode(), directionCode)
@@ -305,7 +309,7 @@ public class ProductServiceImpl implements ProductService {
@VisibleForTesting
ObjectStatusTransitionDO validateProductTransition(String fromStatusCode, String actionCode) {
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
.selectByObjectTypeAndFromStatusAndAction(PRODUCT_OBJECT_TYPE, fromStatusCode, actionCode);
.selectByObjectTypeAndFromStatusAndAction(ProductObjectConstants.OBJECT_TYPE, fromStatusCode, actionCode);
if (transition == null) {
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED, actionCode);
}
@@ -319,10 +323,30 @@ public class ProductServiceImpl implements ProductService {
}
}
@VisibleForTesting
ObjectStatusModelDO validateEnabledStatusModel(String statusCode) {
ObjectStatusModelDO statusModel = objectStatusModelMapper
.selectByObjectTypeAndStatusCodeEnabled(ProductObjectConstants.OBJECT_TYPE, statusCode);
if (statusModel == null) {
throw exception(ErrorCodeConstants.PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
}
return statusModel;
}
@VisibleForTesting
String getInitialStatusCode() {
ObjectStatusModelDO statusModel = objectStatusModelMapper
.selectInitialByObjectTypeEnabled(ProductObjectConstants.OBJECT_TYPE);
if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
throw exception(ErrorCodeConstants.PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
}
return statusModel.getStatusCode();
}
@VisibleForTesting
void validateDeleteConfirmText(String confirmText) {
String normalizedConfirmText = normalizeNullableText(confirmText);
if (!Objects.equals(PRODUCT_DELETE_CONFIRM_TEXT, normalizedConfirmText)) {
if (!Objects.equals(ProductObjectConstants.DELETE_CONFIRM_TEXT, normalizedConfirmText)) {
throw exception(ErrorCodeConstants.PRODUCT_DELETE_CONFIRM_TEXT_INVALID);
}
}
@@ -335,7 +359,7 @@ public class ProductServiceImpl implements ProductService {
}
String year = String.valueOf(LocalDate.now().getYear());
String codePrefix = PRODUCT_CODE_PREFIX + year;
String codePrefix = ProductObjectConstants.CODE_PREFIX + year;
int nextSequence = 1;
for (ProductDO product : productMapper.selectListByCodePrefix(codePrefix)) {
String existedCode = product.getCode();
@@ -366,18 +390,19 @@ public class ProductServiceImpl implements ProductService {
private void initManagerMemberRelation(ProductDO product) {
ObjectRoleRespDTO managerRole = objectPermissionApi
.getObjectRoleByCode(PRODUCT_MANAGER_ROLE_CODE, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
.getObjectRoleByCode(ProductObjectConstants.MANAGER_ROLE_CODE, ObjectRoleConstants.ROLE_SCOPE_OBJECT,
ProductObjectConstants.OBJECT_TYPE)
.getCheckedData();
if (managerRole == null) {
throw invalidParamException("未找到产品经理对象角色配置:{}", PRODUCT_MANAGER_ROLE_CODE);
throw invalidParamException("未找到产品经理对象角色配置:{}", ProductObjectConstants.MANAGER_ROLE_CODE);
}
UserObjectRoleDO member = new UserObjectRoleDO();
member.setUserId(product.getManagerUserId());
member.setObjectType(PRODUCT_OBJECT_TYPE);
member.setObjectType(ProductObjectConstants.OBJECT_TYPE);
member.setObjectId(product.getId());
member.setRoleId(managerRole.getId());
member.setStatus(MEMBER_STATUS_ACTIVE);
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
member.setJoinedTime(LocalDateTime.now());
member.setLeftTime(null);
userObjectRoleMapper.insert(member);
@@ -438,6 +463,35 @@ public class ProductServiceImpl implements ProductService {
.collect(Collectors.toList());
}
private Map<String, Long> buildProductStatusCounts(List<ObjectStatusModelDO> statusModels,
List<Map<String, Object>> rows) {
Map<String, Long> statusCounts = new LinkedHashMap<>();
if (statusModels == null || statusModels.isEmpty()) {
return statusCounts;
}
statusModels.stream()
.filter(statusModel -> Objects.equals(statusModel.getStatus(), 0))
.filter(statusModel -> StringUtils.hasText(statusModel.getStatusCode()))
.sorted(Comparator.comparing(ObjectStatusModelDO::getSort, Comparator.nullsLast(Integer::compareTo))
.thenComparing(ObjectStatusModelDO::getStatusCode))
.forEach(statusModel -> statusCounts.put(statusModel.getStatusCode(), 0L));
if (rows == null || rows.isEmpty()) {
return statusCounts;
}
for (Map<String, Object> row : rows) {
Object statusValue = row.getOrDefault("statusCode", row.get("status_code"));
String statusCode = Objects.toString(statusValue, null);
if (!statusCounts.containsKey(statusCode)) {
continue;
}
Object countValue = row.getOrDefault("countValue", row.get("count_value"));
if (countValue instanceof Number number) {
statusCounts.put(statusCode, number.longValue());
}
}
return statusCounts;
}
private void writeProductStatusLog(ProductDO product, String actionType, String fromStatus,
String toStatus, String reason) {
ProductStatusLogDO statusLog = new ProductStatusLogDO();
@@ -456,7 +510,7 @@ public class ProductServiceImpl implements ProductService {
private void writeBizAuditLog(ProductDO product, String actionType, String fromStatus, String toStatus,
String fieldChanges, String reason) {
BizAuditLogDO auditLog = new BizAuditLogDO();
auditLog.setBizType(PRODUCT_OBJECT_TYPE);
auditLog.setBizType(ProductObjectConstants.OBJECT_TYPE);
auditLog.setBizId(product.getId());
auditLog.setActionType(actionType);
auditLog.setFromStatus(fromStatus);
@@ -481,7 +535,7 @@ public class ProductServiceImpl implements ProductService {
private void writeManagerInitAuditLog(Long productId, Long managerUserId) {
BizAuditLogDO auditLog = new BizAuditLogDO();
auditLog.setBizType(PRODUCT_OBJECT_TYPE);
auditLog.setBizType(ProductObjectConstants.OBJECT_TYPE);
auditLog.setBizId(productId);
auditLog.setActionType(ObjectActivityConstants.PRODUCT_ACTION_CHANGE_MANAGER);
auditLog.setFieldChanges(buildManagerFieldChanges(null, managerUserId));

Some files were not shown because too many files have changed in this diff Show More