Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36752d1d15 | |||
| 73360d70ce | |||
| 7913c210cd | |||
| 06d29210ba | |||
| b4e1aae062 | |||
| 9ad7e063c0 | |||
| 846348e1aa | |||
| ae90dcec24 | |||
| ee732b97bf | |||
| 0a6d70f7cf | |||
| 156728b1b9 | |||
| 2943a6255b | |||
| f8231c2d51 | |||
| a1f5936d20 | |||
| 38c69c748c | |||
| 5815f49a79 | |||
| 0c91f5deaa | |||
| 67040aaf5d | |||
| 8af6842809 | |||
| 9384b2f502 | |||
| 07d07c8f5f | |||
| c3dd0c9802 | |||
| 21ca027f3b | |||
| 017beb1d5f | |||
| 09cba49a7d | |||
| 7e22f79b5f |
@@ -0,0 +1,4 @@
|
||||
#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice.
|
||||
#Tue Apr 21 08:50:58 CST 2026
|
||||
https\://maven.aliyun.com/repository/public/.lastUpdated=1776732658408
|
||||
https\://maven.aliyun.com/repository/public/.error=
|
||||
239
AGENTS.md
Normal file
239
AGENTS.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# AGENTS.md
|
||||
|
||||
## 适用范围
|
||||
|
||||
本说明适用于以 `C:\code\gitea\rdms\cn-rdms` 为根目录的整个仓库。
|
||||
|
||||
描述仓库现状时,以当前代码、当前配置、当前文档中可直接验证的事实为准;除非用户明确要求,不引入历史实现、过渡方案或已废弃模型来解释当前状态。
|
||||
|
||||
默认回答保持精简,优先给结论、改动点和必要风险,不做过多展开;如果存在你关心但未展开的细节,由你继续追问后再补充。
|
||||
|
||||
## 交互原则
|
||||
|
||||
- 默认先给执行方案,说明目标、涉及模块、预计改动点和验证方式。
|
||||
- 在用户评审并明确同意前,不直接开始实际修改、编译、测试、打包或其他执行动作。
|
||||
- 是否执行由用户决定;如果用户只要求分析、审阅或出方案,就停留在分析和方案层。
|
||||
|
||||
## 项目概览
|
||||
|
||||
这是一个面向 RDMS 服务的多模块 Maven 单仓库项目。
|
||||
|
||||
- Java 版本:17
|
||||
- 构建工具:Maven
|
||||
- 根模块打包类型:`pom`
|
||||
- Spring Boot 版本:`3.5.9`
|
||||
|
||||
## 本地环境约定
|
||||
|
||||
- 本机 Maven 安装路径:`C:\software\apache-maven-3.8.9`
|
||||
- 如需执行 Maven 命令,优先使用完整路径:`C:\software\apache-maven-3.8.9\bin\mvn.cmd`
|
||||
- 不要假设 `mvn` 已加入 PATH
|
||||
- 只有在用户已明确同意执行编译、测试、打包等 Maven 命令时,才使用上述路径执行
|
||||
|
||||
顶层模块:
|
||||
|
||||
1. `rdms-system`
|
||||
2. `rdms-project`
|
||||
3. `rdms-framework`
|
||||
4. `rdms-gateway`
|
||||
|
||||
当前系统域能力主要集中在 `rdms-system`,RDMS 核心交付域能力主要集中在 `rdms-project`,但这只是现阶段结构,不应被理解为长期只保留这两个业务模块。
|
||||
|
||||
后续如果新增独立业务服务,例如项目/产品管理模块、工作流模块,应继续沿用当前仓库的模块拆分方式,而不是把所有后续业务长期堆进 `rdms-system`。
|
||||
|
||||
## 模块说明
|
||||
|
||||
### `rdms-system`
|
||||
|
||||
当前已存在的系统业务聚合模块。
|
||||
|
||||
- `rdms-system/rdms-system-boot`
|
||||
- 主应用模块
|
||||
- 启动入口:`rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/SystemServerApplication.java`
|
||||
- 主包路径:`com.njcn.rdms.module.system`
|
||||
- 常见子包:`api`、`controller`、`convert`、`dal`、`framework`、`job`、`service`、`util`、`websocket`
|
||||
- `rdms-system/rdms-system-api`
|
||||
- 供其他服务依赖的共享 API 模块
|
||||
- 包含对外 API 契约与枚举定义
|
||||
|
||||
说明:
|
||||
|
||||
- 当前权限、用户、组织、岗位、菜单、角色等系统核心能力主要落在这里。
|
||||
- 如果后续只是给系统域补充新的系统子能力,可以继续在 `rdms-system` 内按现有结构扩展。
|
||||
- 如果后续形成独立业务域,例如 `rdms-project`、`rdms-workflow`,应优先建设为新的独立业务模块,而不是默认继续塞进 `rdms-system`。
|
||||
|
||||
### `rdms-project`
|
||||
|
||||
当前已存在的 RDMS 核心交付业务聚合模块。
|
||||
|
||||
- `rdms-project/rdms-project-boot`
|
||||
- 主应用模块
|
||||
- 启动入口:`rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java`
|
||||
- 主包路径:`com.njcn.rdms.module.project`
|
||||
- 常见子包:`api`、`controller`、`convert`、`dal`、`framework`、`service`
|
||||
- `rdms-project/rdms-project-api`
|
||||
- 供其他服务依赖的共享 API 模块
|
||||
- 包含对外 API 契约与枚举定义
|
||||
|
||||
说明:
|
||||
|
||||
- 当前项目集、项目、产品、需求、任务、工单、执行等 RDMS 核心业务能力应优先落在这里。
|
||||
- 需要复用用户、组织、岗位、权限等系统能力时,应通过 `rdms-system-api` 调用,不要反向依赖 `rdms-system-boot`。
|
||||
|
||||
### `rdms-framework`
|
||||
|
||||
共享框架与内部 starter 模块。
|
||||
|
||||
- `rdms-framework/rdms-common`
|
||||
- 核心通用工具与公共抽象
|
||||
- 其他 `rdms-spring-boot-starter-*` 模块
|
||||
- 内部 starter,覆盖 `env`、`web`、`rpc`、`security`、`mybatis`、`redis`、`mq`、`websocket`、`excel`、`protection`、`test`、`biz-ip`
|
||||
|
||||
### `rdms-gateway`
|
||||
|
||||
Spring Cloud Gateway 网关服务。
|
||||
|
||||
- 启动入口:`rdms-gateway/src/main/java/com/njcn/rdms/gateway/GatewayServerApplication.java`
|
||||
- 主包路径:`com.njcn.rdms.gateway`
|
||||
- 常见子包:`filter`、`handler`、`jackson`、`route`、`util`
|
||||
|
||||
## 模块演进约束
|
||||
|
||||
后续新增业务能力时,先区分下面两种情况,不要混用:
|
||||
|
||||
1. 新增独立微服务模块,例如 `rdms-project`、`rdms-workflow`
|
||||
2. 只是在现有 `rdms-system` 中新增一个业务子域
|
||||
|
||||
### 新增独立微服务模块
|
||||
|
||||
如果后续能力已经具备独立服务边界,应优先按下面结构建设:
|
||||
|
||||
```text
|
||||
rdms-xxx
|
||||
├─ rdms-xxx-api
|
||||
└─ rdms-xxx-boot
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- 根 `pom.xml` 增加新的聚合模块
|
||||
- `api` 模块承载对外 RPC/Feign 接口、DTO、错误码、枚举、常量
|
||||
- `boot` 模块承载启动类、controller、service、dal、convert、api 实现、模块级 framework 配置和资源文件
|
||||
- 包路径、`spring.application.name`、`ApiConstants`、`RpcConstants`、`rdms.info.base-package` 必须保持一致
|
||||
- 新服务不是简单复制 `rdms-system` 的名字,而是复用它的工程骨架和分层习惯
|
||||
|
||||
### 在 `rdms-system` 中新增业务子域
|
||||
|
||||
如果只是给系统服务补一个当前阶段仍适合放在 `rdms-system` 内的子域,则继续沿用现有结构:
|
||||
|
||||
- `controller/admin/...` 或 `controller/app/...`
|
||||
- `service/...`
|
||||
- `dal/dataobject/...`
|
||||
- `dal/mysql/...`
|
||||
- `convert/...`
|
||||
- 需要跨模块暴露时,在 `rdms-system-api` 中补 API、DTO、错误码、枚举
|
||||
|
||||
约束:
|
||||
|
||||
- 不要为了新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层语言
|
||||
- 不要让外部模块直接依赖 `rdms-system-boot` 的 service 或 mapper
|
||||
- 如果某项能力未来明显会演进成独立微服务,文档和实现上都要避免把它写死成只能存在于 `rdms-system`
|
||||
|
||||
## 代码目录
|
||||
|
||||
- Java 源码:`*/src/main/java`
|
||||
- 资源文件:`*/src/main/resources`
|
||||
- 测试代码:`*/src/test/java`
|
||||
- 本地辅助脚本:`scripts/`
|
||||
|
||||
## 分层职责约束
|
||||
|
||||
### `rdms-framework`
|
||||
|
||||
- `rdms-framework` 承担基础能力,不承载具体业务语义。
|
||||
- 除非出现框架级缺陷,或该能力明确属于全局可复用基础设施,否则不要把业务判断硬塞进 framework。
|
||||
|
||||
### `rdms-gateway`
|
||||
|
||||
- `rdms-gateway` 只负责统一入口、令牌校验、登录用户透传、路由和网关层横切逻辑。
|
||||
- 不要在 gateway 层承载组织、成员、负责人、项目、产品、工作流状态流转或数据可见性这类业务语义。
|
||||
|
||||
### Controller 层
|
||||
|
||||
- Controller 负责 HTTP 暴露、参数校验、权限注解、结果封装。
|
||||
- 不要在 controller 中直接编排复杂业务流程,也不要直接操作多个 mapper 拼装业务规则。
|
||||
- 请求和响应对象优先沿用 `ReqVO`、`RespVO` 风格,不要直接把 DO 暴露给前端。
|
||||
|
||||
### Service 层
|
||||
|
||||
- 核心业务规则、事务、缓存、领域编排应落在 service 层。
|
||||
- 如果是已有领域增强,优先在现有 service 下扩展,不要为了“看起来更整齐”平移整套代码。
|
||||
- 不要把复杂规则散落到 controller、mapper 或 `util` 中。
|
||||
|
||||
### DAL 层
|
||||
|
||||
- 新表应有对应的 DO 和 Mapper。
|
||||
- Mapper 优先继承 `BaseMapperX<T>`,不要重复写样板 CRUD。
|
||||
- 查询条件优先沿用 `LambdaQueryWrapperX`、默认方法封装和现有 MyBatis Plus 风格,不要无必要回退到 XML。
|
||||
- Mapper 以查询封装为主,不承担领域校验职责。
|
||||
|
||||
### Convert 层
|
||||
|
||||
- 如果某个领域已经有 `convert` 风格,则继续沿用。
|
||||
- 简单场景允许直接使用 `BeanUtils`。
|
||||
- 不要为了统一而强推所有地方都改成 MapStruct,也不要反过来把已有 convert 全部删掉。
|
||||
|
||||
## 认证与共享调用约束
|
||||
|
||||
- 默认沿用现有 OAuth2 / Token / `LoginUser` / `login-user` 透传主链,不要另造一套认证上下文体系。
|
||||
- 不要额外发明 ThreadLocal、Session 或自定义 header 体系替代当前登录态恢复方式。
|
||||
- 接口级权限判断默认沿用 `@PreAuthorize("@ss.hasPermission(...)")` 这条链路,不要绕开现有权限框架另起一套实现。
|
||||
- 跨模块、跨服务访问能力时,优先通过对应的 `*-api` 模块定义 API、DTO、常量和枚举。
|
||||
- 不要让外部模块直接依赖某个 `*-boot` 模块的 service 或 mapper。
|
||||
|
||||
## 数据与 SQL 约束
|
||||
|
||||
- 新增业务表的 DO 优先复用当前 `BaseDO` / 审计字段风格;除非表本身明确不需要逻辑删除,不要再引入另一套审计基类。
|
||||
- 不要假设运行时存在自动数据库迁移;如果代码依赖新表、新字段或新索引,必须同步补齐对应 SQL 与文档说明。
|
||||
- SQL 脚本应放在目标模块的 `src/main/resources/sql/...` 下,并保持可审阅、可单独执行、语义清晰。
|
||||
- 变更缓存、日志、审计相关逻辑时,优先沿用现有机制,不要绕开现有登录上下文、缓存约定和审计字段填充方式。
|
||||
|
||||
## 注释与编码
|
||||
|
||||
- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。
|
||||
- 不要为了省事删除原有有效注释,也不要添加无信息量的注释。
|
||||
- 写入中文内容时必须保持 UTF-8 编码,并自行检查中文显示是否正常;不要用“改成英文”规避乱码问题。
|
||||
- 使用 superpowers 产出的功能文档时,例如设计文档、实施计划、联调说明,除非用户明确要求,否则默认用中文落地;代码标识、文件路径、接口路径、SQL、命令保持原始技术标识,不做意译。
|
||||
|
||||
## 工作规则
|
||||
|
||||
1. 除非任务明确要求修改共享契约或 starter,否则优先进行有边界的模块内改动,避免跨模块扩散。
|
||||
2. 业务逻辑应放在对应业务模块的 `*-boot` 实现模块;可复用契约放在对应的 `*-api` 模块;可复用框架能力放在 `rdms-framework`。
|
||||
3. 除非任务本身就是环境配置调整,否则避免修改 `application-local.yaml` 和 `application-dev.yaml`。
|
||||
4. 将本地资源 YAML 视为可能带有机器环境差异的文件;修改前先检查 git 状态。
|
||||
5. 保持既有包结构约定不变:
|
||||
- 控制器放在 `controller`
|
||||
- 服务层放在 `service`
|
||||
- 持久层放在 `dal`
|
||||
- DTO/VO 转换放在 `convert`
|
||||
6. 当前系统域代码主要在 `rdms-system`,RDMS 核心交付域代码主要在 `rdms-project`,但这不是永久约束;新增业务能力时,先判断应该落在现有系统域内、现有项目交付域内,还是应建设为新的 `rdms-xxx` 业务模块。
|
||||
7. 新增共享能力时,优先扩展现有 `rdms-spring-boot-starter-*` 模块,不要在业务服务里重复堆配置。
|
||||
8. 修改跨模块使用的 API 时,需要同时更新提供方实现和对应的 `rdms-system-api` 或对应 `rdms-xxx-api` 契约。
|
||||
9. 除非用户明确要求,否则不执行任何编译、构建、测试、打包或其他会实际运行项目的命令,包括但不限于 `mvn`、启动命令和脚本。
|
||||
|
||||
## 测试指引
|
||||
|
||||
先定义验证方式,再实施修改。默认通过以下方式验证:
|
||||
|
||||
- 代码路径是否闭环,调用链是否与模块边界一致
|
||||
- 配置项、接口契约、权限标识、路由或资源注册是否前后一致
|
||||
- 改动范围是否控制在当前任务所需的最小集合内
|
||||
- 受影响的文档、SQL、配置或接口说明是否需要同步更新
|
||||
|
||||
如果任务影响了 Spring 配置、序列化、安全、路由、RPC 契约、MyBatis 行为或跨模块 API,一律明确说明哪些部分已静态检查、哪些部分尚未实际运行验证。
|
||||
|
||||
## 给后续 Agent 的说明
|
||||
|
||||
- 仓库中可能存在未提交的本地配置改动,不要覆盖与当前任务无关的编辑。
|
||||
- `docs/` 目录属于当前工作上下文的一部分,不是归档材料;做架构级修改前先查阅。
|
||||
- 根目录 `pom.xml` 负责统一版本和依赖对齐;涉及版本调整时,优先修改根 `pom.xml`,不要散落到子模块中。
|
||||
10
pom.xml
10
pom.xml
@@ -9,6 +9,7 @@
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>rdms-system</module>
|
||||
<module>rdms-project</module>
|
||||
<module>rdms-framework</module>
|
||||
<module>rdms-gateway</module>
|
||||
</modules>
|
||||
@@ -118,6 +119,15 @@
|
||||
<artifactId>rdms-spring-boot-starter-websocket</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 热部署依赖-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -26,5 +26,17 @@ public interface RpcConstants {
|
||||
*/
|
||||
String SYSTEM_PREFIX = RPC_API_PREFIX + "/system";
|
||||
|
||||
/**
|
||||
* project 服务名
|
||||
*
|
||||
* 注意,需要保证和 spring.application.name 保持一致
|
||||
*/
|
||||
String PROJECT_NAME = "rdms-project-server";
|
||||
|
||||
/**
|
||||
* project 服务的前缀
|
||||
*/
|
||||
String PROJECT_PREFIX = RPC_API_PREFIX + "/project";
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package com.njcn.rdms.framework.env.core;
|
||||
@@ -10,6 +10,8 @@ import java.util.List;
|
||||
|
||||
public class InDictCollectionValidator implements ConstraintValidator<InDict, Collection<?>> {
|
||||
|
||||
private static final String MISSING_DICT_DATA_MESSAGE = "字典数据缺失,请联系管理员维护";
|
||||
|
||||
private String dictType;
|
||||
|
||||
@Override
|
||||
@@ -25,6 +27,12 @@ public class InDictCollectionValidator implements ConstraintValidator<InDict, Co
|
||||
}
|
||||
// 校验全部通过
|
||||
List<String> dbValues = DictFrameworkUtils.getDictDataValueList(dictType);
|
||||
if (dbValues.isEmpty()) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate(MISSING_DICT_DATA_MESSAGE)
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
boolean match = list.stream().allMatch(v -> dbValues.stream()
|
||||
.anyMatch(dbValue -> dbValue.equalsIgnoreCase(v.toString())));
|
||||
if (match) {
|
||||
@@ -40,4 +48,3 @@ public class InDictCollectionValidator implements ConstraintValidator<InDict, Co
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import java.util.List;
|
||||
|
||||
public class InDictValidator implements ConstraintValidator<InDict, Object> {
|
||||
|
||||
private static final String MISSING_DICT_DATA_MESSAGE = "字典数据缺失,请联系管理员维护";
|
||||
|
||||
private String dictType;
|
||||
|
||||
@Override
|
||||
@@ -24,6 +26,12 @@ public class InDictValidator implements ConstraintValidator<InDict, Object> {
|
||||
}
|
||||
// 校验通过
|
||||
final List<String> values = DictFrameworkUtils.getDictDataValueList(dictType);
|
||||
if (values.isEmpty()) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate(MISSING_DICT_DATA_MESSAGE)
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
boolean match = values.stream().anyMatch(v -> StrUtil.equalsIgnoreCase(v, value.toString()));
|
||||
if (match) {
|
||||
return true;
|
||||
@@ -38,4 +46,3 @@ public class InDictValidator implements ConstraintValidator<InDict, Object> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ spring:
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
config: # 【注册中心】配置项
|
||||
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
|
||||
#################### 监控相关配置 ####################
|
||||
|
||||
@@ -6,10 +6,10 @@ spring:
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
config: # 【注册中心】配置项
|
||||
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
|
||||
#################### 监控相关配置 ####################
|
||||
|
||||
@@ -49,6 +49,19 @@ spring:
|
||||
uri: grayLb://rdms-system-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/system/ws/**
|
||||
## project-server 服务
|
||||
- id: project-admin-api # 路由的编号
|
||||
uri: grayLb://rdms-project-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/admin-api/project/**
|
||||
filters:
|
||||
- RewritePath=/admin-api/project/v3/api-docs, /v3/api-docs
|
||||
- id: project-app-api # 路由的编号
|
||||
uri: grayLb://rdms-project-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/app-api/project/**
|
||||
filters:
|
||||
- RewritePath=/app-api/project/v3/api-docs, /v3/api-docs
|
||||
## bpm-server 服务
|
||||
- id: bpm-admin-api # 路由的编号
|
||||
uri: grayLb://rdms-bpm-server
|
||||
@@ -76,6 +89,9 @@ knife4j:
|
||||
- name: system-server
|
||||
service-name: rdms-system-server
|
||||
url: /admin-api/system/v3/api-docs
|
||||
- name: project-server
|
||||
service-name: rdms-project-server
|
||||
url: /admin-api/project/v3/api-docs
|
||||
- name: bpm-server
|
||||
service-name: bpm-server
|
||||
url: /admin-api/bpm/v3/api-docs
|
||||
|
||||
270
rdms-project/2026-04-23-product-activity-timeline-api-design.md
Normal file
270
rdms-project/2026-04-23-product-activity-timeline-api-design.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# 产品动态时间线后端接口需求说明
|
||||
|
||||
日期: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. 产品经理变更必须提供前后经理信息
|
||||
|
||||
这条接口交付后,前端才能把当前“产品动态时间线”从拼装摘要升级成正式可查询模块。
|
||||
292
rdms-project/2026-04-23-product-overview-homepage-design.md
Normal file
292
rdms-project/2026-04-23-product-overview-homepage-design.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# 产品对象首页改版设计说明
|
||||
|
||||
日期: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 数据源中
|
||||
30
rdms-project/pom.xml
Normal file
30
rdms-project/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>cn-rdms</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>rdms-project</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
RDMS 项目交付域模块
|
||||
该模块承载项目集、项目、产品、需求、任务、工单、执行等 RDMS 核心交付业务能力。
|
||||
</description>
|
||||
<modules>
|
||||
<module>rdms-project-boot</module>
|
||||
<module>rdms-project-api</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
46
rdms-project/rdms-project-api/pom.xml
Normal file
46
rdms-project/rdms-project-api/pom.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-project</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>rdms-project-api</artifactId>
|
||||
<description>
|
||||
项目交付域接口,暴露给其它模块调用
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 参数校验 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 远程调用相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Project API 包,定义暴露给其它模块的 API
|
||||
*/
|
||||
package com.njcn.rdms.module.project.api;
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.njcn.rdms.module.project.enums;
|
||||
|
||||
import com.njcn.rdms.framework.common.enums.RpcConstants;
|
||||
|
||||
/**
|
||||
* API 相关的枚举
|
||||
*/
|
||||
public class ApiConstants {
|
||||
|
||||
/**
|
||||
* 服务名
|
||||
*
|
||||
* 注意,需要保证和 spring.application.name 保持一致
|
||||
*/
|
||||
public static final String NAME = RpcConstants.PROJECT_NAME;
|
||||
|
||||
public static final String PREFIX = RpcConstants.PROJECT_PREFIX;
|
||||
|
||||
public static final String VERSION = "1.0.0";
|
||||
|
||||
private ApiConstants() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.njcn.rdms.module.project.enums;
|
||||
|
||||
import com.njcn.rdms.framework.common.exception.ErrorCode;
|
||||
|
||||
/**
|
||||
* Project 错误码枚举类
|
||||
*
|
||||
* 产品管理当前使用 1-008-001-000 段。
|
||||
*/
|
||||
public interface ErrorCodeConstants {
|
||||
|
||||
// ========== 产品管理 1-008-001-000 ==========
|
||||
ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_008_001_000, "产品不存在");
|
||||
ErrorCode PRODUCT_CODE_DUPLICATE = new ErrorCode(1_008_001_001, "已经存在编码为【{}】的产品");
|
||||
ErrorCode PRODUCT_NAME_DUPLICATE = new ErrorCode(1_008_001_002, "已经存在名称为【{}】的产品");
|
||||
ErrorCode PRODUCT_CODE_NOT_MODIFIABLE = new ErrorCode(1_008_001_003, "产品编码创建后不允许修改");
|
||||
ErrorCode PRODUCT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_001_004, "当前产品状态不支持动作【{}】");
|
||||
ErrorCode PRODUCT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_001_005, "动作【{}】必须填写原因");
|
||||
ErrorCode PRODUCT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_001_006, "删除确认名称与当前产品名称不一致");
|
||||
ErrorCode PRODUCT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_001_007, "当前产品状态不允许编辑");
|
||||
ErrorCode PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE = new ErrorCode(1_008_001_008, "产品暂停后仅允许修正描述,产品经理请通过产品团队维护");
|
||||
ErrorCode PRODUCT_MEMBER_NOT_EXISTS = new ErrorCode(1_008_001_009, "产品团队成员不存在");
|
||||
ErrorCode PRODUCT_MEMBER_ALREADY_EXISTS = new ErrorCode(1_008_001_010, "该用户已是当前产品的有效团队成员");
|
||||
ErrorCode PRODUCT_MEMBER_ROLE_INVALID = new ErrorCode(1_008_001_011, "角色不存在或不属于产品对象角色");
|
||||
ErrorCode PRODUCT_MANAGER_TRANSFER_INFO_REQUIRED = new ErrorCode(1_008_001_013, "切换产品经理时必须同时传入原产品经理用户和交接后角色");
|
||||
ErrorCode PRODUCT_MANAGER_MEMBER_NOT_ALLOW_REMOVE = new ErrorCode(1_008_001_014, "当前产品经理不能移出产品团队,请先完成经理转交");
|
||||
ErrorCode PRODUCT_MANAGER_MEMBER_NOT_ALLOW_DOWNGRADE = new ErrorCode(1_008_001_015, "当前产品经理不能直接调整为非经理角色,请先完成经理转交");
|
||||
ErrorCode PRODUCT_MEMBER_NOT_ACTIVE = new ErrorCode(1_008_001_016, "当前产品团队成员已失效");
|
||||
ErrorCode PRODUCT_MANAGER_TRANSFER_SOURCE_INVALID = new ErrorCode(1_008_001_017, "原产品经理信息与当前产品经理不一致");
|
||||
ErrorCode PRODUCT_MANAGER_TRANSFER_ROLE_INVALID = new ErrorCode(1_008_001_018, "原产品经理交接后的角色不能仍为产品经理");
|
||||
ErrorCode PRODUCT_MANAGER_NOT_MODIFIABLE = new ErrorCode(1_008_001_019, "产品主数据编辑不允许直接变更产品经理,请通过产品团队维护");
|
||||
ErrorCode PRODUCT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_001_020, "当前用户不具备该产品的操作权限【{}】");
|
||||
ErrorCode PRODUCT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_001_021, "删除确认口令不正确");
|
||||
ErrorCode PRODUCT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_001_022, "产品状态已发生变化,请刷新后重试");
|
||||
ErrorCode PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_001_023, "产品状态定义不存在或已停用");
|
||||
|
||||
// ========== 产品需求 1-008-002-000 ==========
|
||||
ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_001, "当前需求状态不支持动作【{}】");
|
||||
ErrorCode REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_002, "动作【{}】必须填写原因");
|
||||
ErrorCode REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_003, "需求状态已发生变化,请刷新后重试");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_004, "当前需求状态为终态,不允许编辑");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_005, "只有已验收的需求才能关闭");
|
||||
ErrorCode REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_006, "需求状态定义不存在或已停用");
|
||||
ErrorCode REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_007, "父需求状态不是待分流或实施中,不允许拆分");
|
||||
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_008, "存在子需求状态不对,请先处理子需求");
|
||||
ErrorCode REQUIREMENT_MODULE_NOT_EXISTS = new ErrorCode(1_008_002_009, "需求模块不存在");
|
||||
ErrorCode REQUIREMENT_MODULE_NAME_DUPLICATE = new ErrorCode(1_008_002_010, "已经存在名称为【{}】的模块");
|
||||
ErrorCode REQUIREMENT_MODULE_NOT_BELONG_TO_PRODUCT = new ErrorCode(1_008_002_011, "模块不属于当前产品");
|
||||
ErrorCode REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS = new ErrorCode(1_008_002_012, "模块下存在非终态需求,不可删除");
|
||||
ErrorCode REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_002_013, "存在子需求,请先删除子需求");
|
||||
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待确认、待评审、待分流状态的需求才能删除");
|
||||
ErrorCode REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_002_015, "存在子模块,请先删除子模块");
|
||||
ErrorCode REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_002_016, "模块下存在需求,请先删除需求");
|
||||
|
||||
}
|
||||
116
rdms-project/rdms-project-boot/pom.xml
Normal file
116
rdms-project/rdms-project-boot/pom.xml
Normal file
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-project</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>rdms-project-boot</artifactId>
|
||||
<description>项目交付域功能服务模块</description>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Cloud 基础 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-env</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 依赖服务 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-project-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-system-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RPC 远程调用相关 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-rpc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-excel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Registry 注册中心相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Config 配置中心相关 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 热部署-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<configuration>
|
||||
<addResources>true</addResources>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.njcn.rdms.module.project;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 项目交付域服务启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class ProjectServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ProjectServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Project API 实现包,放置对外暴露 RPC 接口的实现类
|
||||
*/
|
||||
package com.njcn.rdms.module.project.api;
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 对象动态常量
|
||||
*
|
||||
* 说明:
|
||||
* 1. 当前先承载产品对象首页时间线使用的 activityType / actionType 常量
|
||||
* 2. 后续项目等对象复用同类动态能力时,继续按前缀分组扩展,不单独拆分枚举
|
||||
*/
|
||||
public final class ObjectActivityConstants {
|
||||
|
||||
private ObjectActivityConstants() {
|
||||
}
|
||||
|
||||
// ========== 动态来源类型 ==========
|
||||
public static final String ACTIVITY_TYPE_STATUS = "status";
|
||||
public static final String ACTIVITY_TYPE_PRODUCT = "product";
|
||||
public static final String ACTIVITY_TYPE_MEMBER = "member";
|
||||
|
||||
// ========== 审计业务类型 ==========
|
||||
public static final String PRODUCT_BIZ_TYPE = "product";
|
||||
public static final String MEMBER_BIZ_TYPE = "rdms_user_object_role";
|
||||
|
||||
// ========== 产品对象动作 ==========
|
||||
public static final String PRODUCT_ACTION_CREATE = "create";
|
||||
public static final String PRODUCT_ACTION_UPDATE = "update";
|
||||
public static final String PRODUCT_ACTION_DELETE = "delete";
|
||||
public static final String PRODUCT_ACTION_CHANGE_MANAGER = "change_manager";
|
||||
|
||||
// ========== 状态动作 ==========
|
||||
public static final String STATUS_ACTION_PAUSE = "pause";
|
||||
public static final String STATUS_ACTION_RESUME = "resume";
|
||||
public static final String STATUS_ACTION_ARCHIVE = "archive";
|
||||
public static final String STATUS_ACTION_ABANDON = "abandon";
|
||||
|
||||
// ========== 成员动作 ==========
|
||||
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 List<String> STATUS_ACTION_TYPES = List.of(
|
||||
STATUS_ACTION_PAUSE, STATUS_ACTION_RESUME, STATUS_ACTION_ARCHIVE, STATUS_ACTION_ABANDON);
|
||||
|
||||
public static final List<String> PRODUCT_TIMELINE_ACTION_TYPES = List.of(
|
||||
PRODUCT_ACTION_CREATE, PRODUCT_ACTION_CHANGE_MANAGER);
|
||||
|
||||
public static final List<String> MEMBER_TIMELINE_ACTION_TYPES = List.of(
|
||||
MEMBER_ACTION_ADD, MEMBER_ACTION_UPDATE, MEMBER_ACTION_REMOVE);
|
||||
|
||||
private static final Set<String> STATUS_ACTION_TYPE_SET = Set.copyOf(STATUS_ACTION_TYPES);
|
||||
|
||||
public static boolean isStatusAction(String actionType) {
|
||||
return STATUS_ACTION_TYPE_SET.contains(normalize(actionType));
|
||||
}
|
||||
|
||||
public static String resolveActionName(String actionType) {
|
||||
String normalizedActionType = normalize(actionType);
|
||||
if (!StringUtils.hasText(normalizedActionType)) {
|
||||
return actionType;
|
||||
}
|
||||
return switch (normalizedActionType) {
|
||||
case PRODUCT_ACTION_CREATE -> "创建";
|
||||
case PRODUCT_ACTION_UPDATE -> "更新";
|
||||
case PRODUCT_ACTION_DELETE -> "删除";
|
||||
case STATUS_ACTION_PAUSE -> "暂停";
|
||||
case STATUS_ACTION_RESUME -> "恢复";
|
||||
case STATUS_ACTION_ARCHIVE -> "归档";
|
||||
case STATUS_ACTION_ABANDON -> "废弃";
|
||||
case PRODUCT_ACTION_CHANGE_MANAGER -> "切换产品经理";
|
||||
case MEMBER_ACTION_ADD -> "新增成员";
|
||||
case MEMBER_ACTION_UPDATE -> "调整成员";
|
||||
case MEMBER_ACTION_REMOVE -> "移出成员";
|
||||
default -> normalizedActionType;
|
||||
};
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return StringUtils.hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 管理端控制器包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.controller.admin;
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
|
||||
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;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
import com.njcn.rdms.module.project.service.product.ProductService;
|
||||
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/product")
|
||||
@Validated
|
||||
public class ProductController {
|
||||
|
||||
@Resource
|
||||
private ProductService productService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建产品")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:create')")
|
||||
public CommonResult<Long> createProduct(@Valid @RequestBody ProductSaveReqVO createReqVO) {
|
||||
return success(productService.createProduct(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新产品")
|
||||
public CommonResult<Boolean> updateProduct(@Valid @RequestBody ProductSaveReqVO updateReqVO) {
|
||||
productService.updateProduct(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取产品详情")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<ProductRespVO> getProduct(@RequestParam("id") Long id) {
|
||||
ProductDO product = productService.getProduct(id);
|
||||
return success(BeanUtils.toBean(product, ProductRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/context")
|
||||
@Operation(summary = "获取产品上下文")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<ProductContextRespVO> getProductContext(@PathVariable("id") Long id) {
|
||||
return success(productService.getProductContext(id));
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@PostMapping("/change-status")
|
||||
@Operation(summary = "变更产品状态")
|
||||
public CommonResult<Boolean> changeProductStatus(@Valid @RequestBody ProductStatusActionReqVO reqVO) {
|
||||
productService.changeProductStatus(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除产品")
|
||||
public CommonResult<Boolean> deleteProduct(@Valid @RequestBody ProductDeleteReqVO reqVO) {
|
||||
productService.deleteProduct(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.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;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.service.product.ProductMemberService;
|
||||
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/product")
|
||||
@Validated
|
||||
public class ProductMemberController {
|
||||
|
||||
@Resource
|
||||
private ProductMemberService productMemberService;
|
||||
|
||||
@GetMapping("/{id}/members")
|
||||
@Operation(summary = "获取产品团队成员列表")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<List<ProductMemberRespVO>> getProductMemberList(@PathVariable("id") Long productId) {
|
||||
return success(productMemberService.getProductMemberList(productId));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members")
|
||||
@Operation(summary = "新增产品团队成员")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<Long> createProductMember(@PathVariable("id") Long productId,
|
||||
@Valid @RequestBody ProductMemberSaveReqVO reqVO) {
|
||||
return success(productMemberService.createProductMember(productId, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/members/{memberId}")
|
||||
@Operation(summary = "调整产品团队成员角色")
|
||||
public CommonResult<Boolean> updateProductMember(@PathVariable("id") Long productId,
|
||||
@PathVariable("memberId") Long memberId,
|
||||
@Valid @RequestBody ProductMemberUpdateReqVO reqVO) {
|
||||
productMemberService.updateProductMember(productId, memberId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/{memberId}/inactive")
|
||||
@Operation(summary = "移出产品团队成员")
|
||||
public CommonResult<Boolean> inactiveProductMember(@PathVariable("id") Long productId,
|
||||
@PathVariable("memberId") Long memberId,
|
||||
@Valid @RequestBody ProductMemberInactiveReqVO reqVO) {
|
||||
productMemberService.inactiveProductMember(productId, memberId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
|
||||
import com.njcn.rdms.module.project.service.product.ProductRequirementService;
|
||||
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/product/requirement")
|
||||
@Validated
|
||||
public class ProductRequirementController {
|
||||
|
||||
@Resource
|
||||
private ProductRequirementService requirementService;
|
||||
|
||||
// ========== 需求管理 ==========
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建产品需求")
|
||||
public CommonResult<Long> createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) {
|
||||
return success(requirementService.createRequirement(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新产品需求")
|
||||
public CommonResult<Boolean> updateRequirement(@Valid @RequestBody ProductRequirementUpdateReqVO updateReqVO) {
|
||||
requirementService.updateRequirement(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取需求详情")
|
||||
@Parameter(name = "id", description = "需求编号", required = true, example = "1024")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<ProductRequirementRespVO> getRequirement(@RequestParam("id") Long id,
|
||||
@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getRequirement(id, productId));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取需求分页列表")
|
||||
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementPage(@Valid ProductRequirementPageReqVO pageReqVO) {
|
||||
return success(requirementService.getRequirementPage(pageReqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
@Operation(summary = "获取需求树形列表(分页)")
|
||||
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementTree(@Valid ProductRequirementPageReqVO pageReqVO) {
|
||||
return success(requirementService.getRequirementTree(pageReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/change-status")
|
||||
@Operation(summary = "变更需求状态")
|
||||
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) {
|
||||
requirementService.changeRequirementStatus(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除产品需求")
|
||||
public CommonResult<Boolean> deleteRequirement(@Valid @RequestBody ProductRequirementDeleteReqVO reqVO) {
|
||||
requirementService.deleteRequirement(reqVO.getId(), reqVO.getProductId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/split")
|
||||
@Operation(summary = "拆分产品需求")
|
||||
public CommonResult<Long> splitRequirement(@Valid @RequestBody ProductRequirementSplitReqVO reqVO) {
|
||||
System.out.println("-----------------------");
|
||||
System.out.println(reqVO);
|
||||
return success(requirementService.splitRequirement(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/close")
|
||||
@Operation(summary = "关闭产品需求")
|
||||
public CommonResult<Boolean> closeRequirement(@Valid @RequestBody ProductRequirementCloseReqVO reqVO) {
|
||||
requirementService.closeRequirement(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/allowed-transitions")
|
||||
@Operation(summary = "获取需求可执行的状态动作列表")
|
||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<List<ProductRequirementStatusTransitionRespVO>> getAllowedTransitions(
|
||||
@RequestParam("requirementId") Long requirementId,
|
||||
@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getAllowedTransitions(requirementId, productId));
|
||||
}
|
||||
|
||||
@GetMapping("/lifecycle")
|
||||
@Operation(summary = "获取需求生命周期信息")
|
||||
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<ProductRequirementLifecycleRespVO> getRequirementLifecycle(
|
||||
@RequestParam("requirementId") Long requirementId,
|
||||
@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getRequirementLifecycle(requirementId, productId));
|
||||
}
|
||||
|
||||
// ========== 模块管理 ==========
|
||||
|
||||
@PostMapping("/module/create")
|
||||
@Operation(summary = "创建需求模块")
|
||||
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
|
||||
return success(requirementService.createRequirementModule(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/module/update")
|
||||
@Operation(summary = "更新需求模块")
|
||||
public CommonResult<Boolean> updateRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
|
||||
requirementService.updateRequirementModule(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/module/delete")
|
||||
@Operation(summary = "删除需求模块")
|
||||
public CommonResult<Boolean> deleteRequirementModule(@Valid @RequestBody ProductRequirementModuleDeleteReqVO reqVO) {
|
||||
requirementService.deleteRequirementModule(reqVO.getId(), reqVO.getProductId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/module/tree")
|
||||
@Operation(summary = "获取需求模块树")
|
||||
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<List<ProductRequirementModuleRespVO>> getRequirementModuleTree(@RequestParam("productId") Long productId) {
|
||||
return success(requirementService.getRequirementModuleTree(productId));
|
||||
}
|
||||
|
||||
@GetMapping("/status/dict")
|
||||
@Operation(summary = "获取需求所有状态字典")
|
||||
public CommonResult<List<ProductRequirementStatusDictRespVO>> getRequirementStatusDict() {
|
||||
return success(requirementService.getRequirementStatusDict());
|
||||
}
|
||||
|
||||
@GetMapping("/status/dict/terminal")
|
||||
@Operation(summary = "获取需求终止态状态字典")
|
||||
public CommonResult<List<ProductRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
|
||||
return success(requirementService.getRequirementTerminalStatusDict());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
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.controller.admin.product.vo.activity.ProductActivityTimelinePageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelineRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingRespVO;
|
||||
import com.njcn.rdms.module.project.service.product.ProductService;
|
||||
import com.njcn.rdms.module.project.service.product.ProductSettingService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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/product")
|
||||
@Validated
|
||||
public class ProductSettingController {
|
||||
|
||||
@Resource
|
||||
private ProductSettingService productSettingService;
|
||||
@Resource
|
||||
private ProductService productService;
|
||||
|
||||
@GetMapping("/{id}/settings")
|
||||
@Operation(summary = "获取产品设置")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<ProductSettingRespVO> getProductSettings(@PathVariable("id") Long id) {
|
||||
return success(productSettingService.getProductSettings(id));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/activities")
|
||||
@Operation(summary = "获取产品动态")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<PageResult<ProductActivityRespVO>> getProductActivities(@PathVariable("id") Long id,
|
||||
@Valid ProductActivityPageReqVO reqVO) {
|
||||
return success(productSettingService.getProductActivities(id, reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/activities/page")
|
||||
@Operation(summary = "获取产品动态时间线分页")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<PageResult<ProductActivityTimelineRespVO>> getProductActivityTimelinePage(
|
||||
@PathVariable("id") Long id, @Valid ProductActivityTimelinePageReqVO reqVO) {
|
||||
return success(productSettingService.getProductActivityTimelinePage(id, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/settings/base-info")
|
||||
@Operation(summary = "更新产品设置基础信息")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> updateProductBaseInfo(@PathVariable("id") Long id,
|
||||
@Valid @RequestBody ProductSettingBaseInfoUpdateReqVO reqVO) {
|
||||
productService.updateProductBaseInfo(id, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.activity;
|
||||
|
||||
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 ProductActivityPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "动态类型", example = "status")
|
||||
@Size(max = 16, message = "动态类型长度不能超过16个字符")
|
||||
private String activityType;
|
||||
|
||||
@Schema(description = "动作编码", example = "pause")
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionType;
|
||||
|
||||
@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[] operateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.activity;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 产品动态 Response VO")
|
||||
@Data
|
||||
public class ProductActivityRespVO {
|
||||
|
||||
@Schema(description = "动态类型", example = "status")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "动作编码", example = "pause")
|
||||
private String actionType;
|
||||
|
||||
@Schema(description = "动作名称", example = "暂停")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "原状态", example = "active")
|
||||
private String fromStatus;
|
||||
|
||||
@Schema(description = "目标状态", example = "paused")
|
||||
private String toStatus;
|
||||
|
||||
@Schema(description = "动作原因", example = "资源不足")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "操作人用户编号", example = "1024")
|
||||
private Long operatorUserId;
|
||||
|
||||
@Schema(description = "操作人名称", example = "张三")
|
||||
private String operatorName;
|
||||
|
||||
@Schema(description = "操作时间", example = "2026-04-21 12:00:00")
|
||||
private LocalDateTime operateTime;
|
||||
|
||||
@Schema(description = "展示摘要", example = "张三执行了【暂停】:资源不足")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "补充详情")
|
||||
private String details;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.activity;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 产品动态时间线分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductActivityTimelinePageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "动态类型", example = "status")
|
||||
@Size(max = 16, message = "动态类型长度不能超过16个字符")
|
||||
private String activityType;
|
||||
|
||||
@Schema(description = "动作编码数组")
|
||||
private List<@Size(max = 32, message = "动作编码长度不能超过32个字符") String> actionTypes;
|
||||
|
||||
@Schema(description = "开始时间", example = "2026-03-24 00:00:00")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Schema(description = "结束时间", example = "2026-04-23 23:59:59")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime endTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.activity;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 产品动态时间线 Response VO")
|
||||
@Data
|
||||
public class ProductActivityTimelineRespVO {
|
||||
|
||||
@Schema(description = "动态唯一标识", example = "status:11")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "动态类型", example = "status")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "动作编码", example = "pause")
|
||||
private String actionType;
|
||||
|
||||
@Schema(description = "动作名称", example = "暂停")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "操作人用户编号", example = "1024")
|
||||
private Long operatorUserId;
|
||||
|
||||
@Schema(description = "操作人名称", example = "张三")
|
||||
private String operatorName;
|
||||
|
||||
@Schema(description = "目标成员用户编号,仅 member 类型返回", example = "2043945809271713793")
|
||||
private Long targetUserId;
|
||||
|
||||
@Schema(description = "目标成员名称,仅 member 类型返回,读取缓存实时转换", example = "张三")
|
||||
private String targetUserName;
|
||||
|
||||
@Schema(description = "发生时间", example = "2026-04-21 12:00:00")
|
||||
private LocalDateTime occurredAt;
|
||||
|
||||
@Schema(description = "展示摘要", example = "张三执行了【暂停】:资源不足")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "动作原因", example = "资源不足")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "原状态", example = "active")
|
||||
private String fromStatus;
|
||||
|
||||
@Schema(description = "目标状态", example = "paused")
|
||||
private String toStatus;
|
||||
|
||||
@Schema(description = "补充详情")
|
||||
private String details;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品团队成员移出 Request VO")
|
||||
@Data
|
||||
public class ProductMemberInactiveReqVO {
|
||||
|
||||
@Schema(description = "移出原因", example = "已退出当前产品协作")
|
||||
@Size(max = 500, message = "移出原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 产品团队成员 Response VO")
|
||||
@Data
|
||||
public class ProductMemberRespVO {
|
||||
|
||||
@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 = "product_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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.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 ProductMemberSaveReqVO {
|
||||
|
||||
@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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.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 ProductMemberUpdateReqVO {
|
||||
|
||||
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002002")
|
||||
@NotNull(message = "角色编号不能为空")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "备注", example = "调整为产品观察者")
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "变更原因", example = "职责调整")
|
||||
@Size(max = 500, message = "变更原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "原产品经理用户编号,仅切换产品经理时传递", example = "2048")
|
||||
private Long previousManagerUserId;
|
||||
|
||||
@Schema(description = "原产品经理交接后的角色编号,仅切换产品经理时传递", example = "3100000002002")
|
||||
private Long previousManagerRoleId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品上下文导航 Response VO")
|
||||
@Data
|
||||
public class ProductContextNavRespVO {
|
||||
|
||||
@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/product/overview")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "菜单图标", example = "mdi:view-dashboard-outline")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "显示顺序", example = "10")
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品上下文中的当前产品摘要 Response VO")
|
||||
@Data
|
||||
public class ProductContextProductRespVO {
|
||||
|
||||
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPD2026001")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
|
||||
private String directionCode;
|
||||
|
||||
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "产品经理用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long managerUserId;
|
||||
|
||||
@Schema(description = "产品状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||
private String statusCode;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
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 ProductContextRespVO {
|
||||
|
||||
@Schema(description = "当前产品摘要")
|
||||
private ProductContextProductRespVO currentProduct;
|
||||
|
||||
@Schema(description = "当前用户在该产品下的角色信息")
|
||||
private ProductContextRoleRespVO currentRole;
|
||||
|
||||
@Schema(description = "当前产品下可见导航集合")
|
||||
private List<ProductContextNavRespVO> navs;
|
||||
|
||||
@Schema(description = "当前产品下按钮权限码集合")
|
||||
private List<String> buttons;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品上下文中的当前角色 Response VO")
|
||||
@Data
|
||||
public class ProductContextRoleRespVO {
|
||||
|
||||
@Schema(description = "对象角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3201")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "对象角色编码", example = "product_manager")
|
||||
private String roleCode;
|
||||
|
||||
@Schema(description = "对象角色名称", example = "产品经理")
|
||||
private String roleName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
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 ProductDeleteReqVO {
|
||||
|
||||
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "确认输入的产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
|
||||
@NotBlank(message = "确认产品名称不能为空")
|
||||
@Size(max = 128, message = "确认产品名称长度不能超过128个字符")
|
||||
private String productName;
|
||||
|
||||
@Schema(description = "删除原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品录入错误")
|
||||
@NotBlank(message = "删除原因不能为空")
|
||||
@Size(max = 500, message = "删除原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "删除确认口令,当前固定输入 DELETE", requiredMode = Schema.RequiredMode.REQUIRED, example = "DELETE")
|
||||
@NotBlank(message = "删除确认口令不能为空")
|
||||
@Size(max = 32, message = "删除确认口令长度不能超过32个字符")
|
||||
private String confirmText;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
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 ProductPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "关键词,匹配产品编码或产品名称", example = "CNPD2026001")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "产品方向字典值", example = "direction_value")
|
||||
@InDict(type = DictTypeConstants.OBJECT_DIRECTION)
|
||||
private String directionCode;
|
||||
|
||||
@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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 产品 Response VO")
|
||||
@Data
|
||||
public class ProductRespVO {
|
||||
|
||||
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPD2026001")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
|
||||
private String directionCode;
|
||||
|
||||
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "产品经理用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long managerUserId;
|
||||
|
||||
@Schema(description = "产品描述", example = "面向研发管理的一体化产品")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "产品状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "阶段性暂停")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
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.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品保存 Request VO")
|
||||
@Data
|
||||
public class ProductSaveReqVO {
|
||||
|
||||
@Schema(description = "产品编号", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "产品编码,为空时由系统自动生成", example = "CNPD2026001")
|
||||
@Size(max = 64, message = "产品编码长度不能超过64个字符")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
|
||||
@NotBlank(message = "产品方向不能为空")
|
||||
@Size(max = 32, message = "产品方向长度不能超过32个字符")
|
||||
@InDict(type = DictTypeConstants.OBJECT_DIRECTION)
|
||||
private String directionCode;
|
||||
|
||||
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
|
||||
@NotBlank(message = "产品名称不能为空")
|
||||
@Size(max = 128, message = "产品名称长度不能超过128个字符")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "产品经理用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品经理不能为空")
|
||||
private Long managerUserId;
|
||||
|
||||
@Schema(description = "产品描述", example = "面向研发管理的一体化产品")
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
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 ProductStatusActionReqVO {
|
||||
|
||||
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "动作编码,如 pause、resume、archive、abandon", requiredMode = Schema.RequiredMode.REQUIRED, example = "pause")
|
||||
@NotBlank(message = "动作编码不能为空")
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作原因;是否必填由状态流转配置决定", example = "当前阶段受环境限制暂停推进")
|
||||
@Size(max = 500, message = "动作原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求关闭 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求关闭 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementCloseReqVO {
|
||||
|
||||
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "需求编号不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "关闭原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "需求已完成验收")
|
||||
@NotBlank(message = "关闭原因不能为空")
|
||||
@Size(max = 255, message = "关闭原因长度不能超过255个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求删除 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求删除 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementDeleteReqVO {
|
||||
@Schema(description = "需求ID(编辑时传入)", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求生命周期 Response VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求生命周期 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementLifecycleRespVO {
|
||||
|
||||
@Schema(description = "当前状态编码", example = "pending_dispatch")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "当前状态名称", example = "待分流")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
@Schema(description = "是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
|
||||
@Schema(description = "当前状态可执行动作列表")
|
||||
private List<ProductRequirementStatusTransitionRespVO> availableActions;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求模块删除 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求模块删除 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementModuleDeleteReqVO {
|
||||
@Schema(description = "模块ID(编辑时传入)", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求模块保存 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求模块保存 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementModuleReqVO {
|
||||
|
||||
@Schema(description = "模块ID(编辑时传入)", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "父模块ID(0表示顶级)", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能")
|
||||
@NotBlank(message = "模块名称不能为空")
|
||||
@Size(max = 100, message = "模块名称长度不能超过100个字符")
|
||||
private String moduleName;
|
||||
|
||||
@Schema(description = "模块说明", example = "产品核心功能模块")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "图标", example = "icon-function")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "排序值", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求模块 Response VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求模块 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementModuleRespVO {
|
||||
|
||||
@Schema(description = "模块ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "父模块ID(0表示顶级)", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能")
|
||||
private String moduleName;
|
||||
|
||||
@Schema(description = "模块说明", example = "产品核心功能模块")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "图标", example = "icon-function")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "排序值", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "子模块列表")
|
||||
private List<ProductRequirementModuleRespVO> children;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求分页 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductRequirementPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "所属模块ID列表(包含子模块,用于IN查询)", example = "[1024, 1025]")
|
||||
private List<Long> moduleIds;
|
||||
|
||||
@Schema(description = "父需求ID(查询子需求时使用)", example = "1024")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "标题关键词", example = "模块")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求分类字典值", example = "function")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", example = "1")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "状态编码", example = "pending_dispatch")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "来源类型(manual:手工新增, work_order:工单流转)", example = "manual")
|
||||
private String sourceType;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求 Response VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementRespVO {
|
||||
|
||||
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "父需求ID(0表示顶级需求)", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "所属模块ID", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", example = "0")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "需求分类名称", example = "功能需求")
|
||||
private String categoryName;
|
||||
|
||||
@Schema(description = "来源类型(manual:手工新增, work_order:工单流转)", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "来源业务ID", example = "1024")
|
||||
private Long sourceBizId;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "优先级名称", example = "中")
|
||||
private String priorityName;
|
||||
|
||||
@Schema(description = "当前状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending_dispatch")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "当前状态名称", example = "待分流")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "评审通过")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "提出人用户姓名", example = "张三")
|
||||
private String proposerNickname;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "当前处理人姓名", example = "李四")
|
||||
private String currentHandlerUserNickname;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "实现项目名称", example = "NPQS-10086")
|
||||
private String implementProjectName;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "子需求列表(树形结构)")
|
||||
private List<ProductRequirementRespVO> children;
|
||||
|
||||
@Schema(description = "是否为终态(已拒绝、已取消、已关闭)", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求保存 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求保存 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementSaveReqVO {
|
||||
|
||||
@Schema(description = "需求ID", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
@NotBlank(message = "需求标题不能为空")
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求拆分 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求拆分 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementSplitReqVO {
|
||||
|
||||
@Schema(description = "父需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "父需求编号不能为空")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
@NotBlank(message = "需求标题不能为空")
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求状态变更 Request VO
|
||||
* 注意:需求不直接关联产品,通过模块间接关联
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求状态变更 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementStatusActionReqVO {
|
||||
|
||||
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "需求ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "dispatch")
|
||||
@NotBlank(message = "动作编码不能为空")
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "状态变更原因", example = "评审通过,进入分流阶段")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "实现项目编号(dispatch动作时可选)", example = "1024")
|
||||
private Long implementProjectId;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求状态字典 Response VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求状态字典 Response VO")
|
||||
@Data
|
||||
public class ProductRequirementStatusDictRespVO {
|
||||
|
||||
@Schema(description = "状态编码", example = "pending_confirm")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "状态名称", example = "待确认")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "排序值", example = "1")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "是否初始状态", example = "true")
|
||||
private Boolean initialFlag;
|
||||
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminalFlag;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求状态可执行动作 Response VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求状态可执行动作 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class ProductRequirementStatusTransitionRespVO {
|
||||
|
||||
@Schema(description = "动作编码", example = "dispatch")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "明确分流/拆分")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "目标状态编码", example = "implementing")
|
||||
private String toStatusCode;
|
||||
|
||||
@Schema(description = "目标状态名称", example = "实施中")
|
||||
private String toStatusName;
|
||||
|
||||
@Schema(description = "是否必须填写原因", example = "false")
|
||||
private Boolean needReason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
|
||||
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理后台 - 产品需求编辑 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - 产品需求编辑 Request VO")
|
||||
@Data
|
||||
public class ProductRequirementUpdateReqVO {
|
||||
|
||||
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "需求ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "所属模块ID(为空时归入全部需求)", example = "1024")
|
||||
private Long moduleId;
|
||||
|
||||
@Schema(description = "是否需要评审(0不需要;1需要)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "是否需要评审不能为空")
|
||||
private Integer reviewRequired;
|
||||
|
||||
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
|
||||
@NotBlank(message = "需求标题不能为空")
|
||||
@Size(max = 200, message = "需求标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "需求描述(富文本)", example = "<p>详细描述需求内容</p>")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
|
||||
@NotBlank(message = "需求分类不能为空")
|
||||
@Size(max = 64, message = "需求分类长度不能超过64个字符")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "优先级(0低 1中 2高 3紧急)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "优先级不能为空")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "提出人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "提出人不能为空")
|
||||
private Long proposerId;
|
||||
|
||||
@Schema(description = "当前处理人用户编号", example = "1024")
|
||||
private Long currentHandlerUserId;
|
||||
|
||||
@Schema(description = "默认实现项目编号", example = "1024")
|
||||
private Long implementProjectId;
|
||||
|
||||
@Schema(description = "预期完成时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "预期完成时间不能为空")
|
||||
private LocalDateTime completionDate;
|
||||
|
||||
@Schema(description = "排序值(越小越靠前)", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "管理后台 - 产品设置生命周期动作 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class ProductSettingActionRespVO {
|
||||
|
||||
@Schema(description = "动作编码", example = "archive")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "归档")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "是否必须填写原因", example = "true")
|
||||
private Boolean needReason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "管理后台 - 产品设置基础信息 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class ProductSettingBaseInfoRespVO {
|
||||
|
||||
@Schema(description = "产品编号", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "产品编码", example = "CNPD2026001")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "产品方向字典值", example = "direction_value")
|
||||
private String directionCode;
|
||||
|
||||
@Schema(description = "产品名称", example = "统一交付平台")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "产品经理用户编号", example = "10001")
|
||||
private Long managerUserId;
|
||||
|
||||
@Schema(description = "产品经理昵称", example = "张三")
|
||||
private String managerUserNickname;
|
||||
|
||||
@Schema(description = "产品描述", example = "产品描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "产品状态编码", example = "active")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "恢复正常推进")
|
||||
private String lastStatusReason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
|
||||
|
||||
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.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品设置基础信息更新 Request VO")
|
||||
@Data
|
||||
public class ProductSettingBaseInfoUpdateReqVO {
|
||||
|
||||
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
|
||||
@NotBlank(message = "产品方向不能为空")
|
||||
@Size(max = 32, message = "产品方向长度不能超过32个字符")
|
||||
@InDict(type = DictTypeConstants.OBJECT_DIRECTION)
|
||||
private String directionCode;
|
||||
|
||||
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
|
||||
@NotBlank(message = "产品名称不能为空")
|
||||
@Size(max = 128, message = "产品名称长度不能超过128个字符")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "产品描述", example = "面向研发管理的一体化产品")
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
|
||||
|
||||
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 ProductSettingLifecycleRespVO {
|
||||
|
||||
@Schema(description = "当前状态编码", example = "active")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "当前状态名称", example = "启用")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "恢复正常推进")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
@Schema(description = "是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
|
||||
@Schema(description = "当前状态可执行动作")
|
||||
private List<ProductSettingActionRespVO> availableActions;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "管理后台 - 产品设置 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class ProductSettingRespVO {
|
||||
|
||||
@Schema(description = "产品基础信息")
|
||||
private ProductSettingBaseInfoRespVO baseInfo;
|
||||
|
||||
@Schema(description = "产品生命周期信息")
|
||||
private ProductSettingLifecycleRespVO lifecycle;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 应用端控制器包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.controller.app;
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 提供 RESTful API 给前端:
|
||||
* 1. admin 包:提供给管理后台 rdms-ui-admin 前端项目
|
||||
* 2. app 包:提供给用户 APP rdms-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分
|
||||
*/
|
||||
package com.njcn.rdms.module.project.controller;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* DTO、VO、DO 等对象转换包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.convert;
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.audit;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* RDMS通用业务审计日志表
|
||||
*/
|
||||
@TableName("rdms_biz_audit_log")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BizAuditLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 业务对象类型
|
||||
*/
|
||||
private String bizType;
|
||||
/**
|
||||
* 业务对象ID
|
||||
*/
|
||||
private Long bizId;
|
||||
/**
|
||||
* 动作类型
|
||||
*/
|
||||
private String actionType;
|
||||
/**
|
||||
* 原状态
|
||||
*/
|
||||
private String fromStatus;
|
||||
/**
|
||||
* 目标状态
|
||||
*/
|
||||
private String toStatus;
|
||||
/**
|
||||
* 关键字段变更摘要
|
||||
*/
|
||||
private String fieldChanges;
|
||||
/**
|
||||
* 动作原因或说明
|
||||
*/
|
||||
private String reason;
|
||||
/**
|
||||
* 操作人用户ID
|
||||
*/
|
||||
private Long operatorUserId;
|
||||
/**
|
||||
* 操作人名称快照
|
||||
*/
|
||||
private String operatorName;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.member;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* RDMS 对象成员角色关系表
|
||||
*/
|
||||
@TableName("rdms_user_object_role")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserObjectRoleDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 对象类型
|
||||
*/
|
||||
private String objectType;
|
||||
/**
|
||||
* 对象ID
|
||||
*/
|
||||
private Long objectId;
|
||||
/**
|
||||
* 对象角色ID
|
||||
*/
|
||||
private Long roleId;
|
||||
/**
|
||||
* 状态(0有效 1失效)
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 加入时间
|
||||
*/
|
||||
private LocalDateTime joinedTime;
|
||||
/**
|
||||
* 退出时间
|
||||
*/
|
||||
private LocalDateTime leftTime;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 数据对象包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.dal.dataobject;
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.product;
|
||||
|
||||
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_product")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 产品编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 产品编码
|
||||
*/
|
||||
private String code;
|
||||
/**
|
||||
* 产品方向字典值
|
||||
*/
|
||||
private String directionCode;
|
||||
/**
|
||||
* 产品状态编码
|
||||
*/
|
||||
private String statusCode;
|
||||
/**
|
||||
* 产品名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 产品经理用户编号
|
||||
*/
|
||||
private Long managerUserId;
|
||||
/**
|
||||
* 产品描述
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 最近一次状态动作原因
|
||||
*/
|
||||
private String lastStatusReason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.product;
|
||||
|
||||
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_product_requirement")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductRequirementDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 父需求ID(0表示顶级需求)
|
||||
*/
|
||||
private Long parentId;
|
||||
/**
|
||||
* 所属模块ID(0表示全部需求)
|
||||
*/
|
||||
private Long moduleId;
|
||||
/**
|
||||
* 所属产品ID
|
||||
*/
|
||||
private Long productId;
|
||||
/**
|
||||
* 是否需要评审(0不需要;1需要)
|
||||
*/
|
||||
private Integer reviewRequired;
|
||||
/**
|
||||
* 需求标题
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 需求描述(富文本)
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 需求分类字典值
|
||||
*/
|
||||
private String category;
|
||||
/**
|
||||
* 来源类型(manual:手工新增, work_order:工单流转)
|
||||
*/
|
||||
private String sourceType;
|
||||
/**
|
||||
* 来源业务ID
|
||||
*/
|
||||
private Long sourceBizId;
|
||||
/**
|
||||
* 优先级(0低 1中 2高 3紧急)
|
||||
*/
|
||||
private Integer priority;
|
||||
/**
|
||||
* 当前状态编码
|
||||
*/
|
||||
private String statusCode;
|
||||
/**
|
||||
* 最近一次状态动作原因
|
||||
*/
|
||||
private String lastStatusReason;
|
||||
/**
|
||||
* 提出人用户ID
|
||||
*/
|
||||
private Long proposerId;
|
||||
/**
|
||||
* 提出人用户姓名快照
|
||||
*/
|
||||
private String proposerNickname;
|
||||
/**
|
||||
* 当前处理人用户ID
|
||||
*/
|
||||
private Long currentHandlerUserId;
|
||||
/**
|
||||
* 当前处理人姓名快照
|
||||
*/
|
||||
private String currentHandlerUserNickname;
|
||||
/**
|
||||
* 默认实现项目ID(分流后填写)
|
||||
*/
|
||||
private Long implementProjectId;
|
||||
/**
|
||||
* 预期完成时间
|
||||
*/
|
||||
private LocalDateTime completionDate;
|
||||
/**
|
||||
* 排序值(越小越靠前)
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.product;
|
||||
|
||||
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_product_requirement_module")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductRequirementModuleDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 父模块ID(0表示顶级)
|
||||
*/
|
||||
private Long parentId;
|
||||
/**
|
||||
* 所属产品ID
|
||||
*/
|
||||
private Long productId;
|
||||
/**
|
||||
* 模块名称
|
||||
*/
|
||||
private String moduleName;
|
||||
/**
|
||||
* 模块说明
|
||||
*/
|
||||
private String remark;
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
private String icon;
|
||||
/**
|
||||
* 排序值
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.product;
|
||||
|
||||
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_product_requirement_status_log")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductRequirementStatusLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 需求ID
|
||||
*/
|
||||
private Long requirementId;
|
||||
/**
|
||||
* 动作编码
|
||||
*/
|
||||
private String actionType;
|
||||
/**
|
||||
* 变更前状态
|
||||
*/
|
||||
private String fromStatus;
|
||||
/**
|
||||
* 变更后状态
|
||||
*/
|
||||
private String toStatus;
|
||||
/**
|
||||
* 动作原因
|
||||
*/
|
||||
private String reason;
|
||||
/**
|
||||
* 操作人ID
|
||||
*/
|
||||
private Long operatorUserId;
|
||||
/**
|
||||
* 操作人名称快照
|
||||
*/
|
||||
private String operatorName;
|
||||
/**
|
||||
* 需求标题快照
|
||||
*/
|
||||
private String requirementTitleSnapshot;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.product;
|
||||
|
||||
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_product_status_log")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductStatusLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 产品ID
|
||||
*/
|
||||
private Long productId;
|
||||
/**
|
||||
* 动作类型
|
||||
*/
|
||||
private String actionType;
|
||||
/**
|
||||
* 变更前状态编码
|
||||
*/
|
||||
private String fromStatus;
|
||||
/**
|
||||
* 变更后状态编码
|
||||
*/
|
||||
private String toStatus;
|
||||
/**
|
||||
* 动作原因
|
||||
*/
|
||||
private String reason;
|
||||
/**
|
||||
* 操作人用户ID
|
||||
*/
|
||||
private Long operatorUserId;
|
||||
/**
|
||||
* 操作人名称快照
|
||||
*/
|
||||
private String operatorName;
|
||||
/**
|
||||
* 产品编码快照
|
||||
*/
|
||||
private String productCodeSnapshot;
|
||||
/**
|
||||
* 产品名称快照
|
||||
*/
|
||||
private String productNameSnapshot;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.status;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* RDMS对象状态模型表
|
||||
*/
|
||||
@TableName("rdms_object_status_model")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ObjectStatusModelDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 对象类型
|
||||
*/
|
||||
private String objectType;
|
||||
/**
|
||||
* 状态编码
|
||||
*/
|
||||
private String statusCode;
|
||||
/**
|
||||
* 状态名称
|
||||
*/
|
||||
private String statusName;
|
||||
/**
|
||||
* 排序值
|
||||
*/
|
||||
private Integer sort;
|
||||
/**
|
||||
* 配置状态
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 是否初始状态
|
||||
*/
|
||||
private Boolean initialFlag;
|
||||
/**
|
||||
* 是否终态
|
||||
*/
|
||||
private Boolean terminalFlag;
|
||||
/**
|
||||
* 是否允许编辑对象主数据
|
||||
*/
|
||||
private Boolean allowEdit;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.status;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* RDMS对象状态流转表
|
||||
*/
|
||||
@TableName("rdms_object_status_transition")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ObjectStatusTransitionDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 对象类型
|
||||
*/
|
||||
private String objectType;
|
||||
/**
|
||||
* 动作编码
|
||||
*/
|
||||
private String actionCode;
|
||||
/**
|
||||
* 动作名称
|
||||
*/
|
||||
private String actionName;
|
||||
/**
|
||||
* 起始状态编码
|
||||
*/
|
||||
private String fromStatusCode;
|
||||
/**
|
||||
* 目标状态编码
|
||||
*/
|
||||
private String toStatusCode;
|
||||
/**
|
||||
* 是否必须填写原因
|
||||
*/
|
||||
private Boolean needReason;
|
||||
/**
|
||||
* 配置状态
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.audit;
|
||||
|
||||
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.audit.BizAuditLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface BizAuditLogMapper extends BaseMapperX<BizAuditLogDO> {
|
||||
|
||||
default List<BizAuditLogDO> selectListByBiz(String bizType, Long bizId, String actionType, LocalDateTime[] operateTime) {
|
||||
return selectList(new LambdaQueryWrapperX<BizAuditLogDO>()
|
||||
.eq(BizAuditLogDO::getBizType, bizType)
|
||||
.eq(BizAuditLogDO::getBizId, bizId)
|
||||
.eqIfPresent(BizAuditLogDO::getActionType, actionType)
|
||||
.betweenIfPresent(BaseDO::getCreateTime, operateTime)
|
||||
.orderByDesc(BaseDO::getCreateTime)
|
||||
.orderByDesc(BizAuditLogDO::getId));
|
||||
}
|
||||
|
||||
default List<BizAuditLogDO> selectListByBizAndActions(String bizType, Long bizId, List<String> actionTypes,
|
||||
LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return selectList(new LambdaQueryWrapperX<BizAuditLogDO>()
|
||||
.eq(BizAuditLogDO::getBizType, bizType)
|
||||
.eq(BizAuditLogDO::getBizId, bizId)
|
||||
.inIfPresent(BizAuditLogDO::getActionType, actionTypes)
|
||||
.betweenIfPresent(BaseDO::getCreateTime, startTime, endTime)
|
||||
.orderByDesc(BaseDO::getCreateTime)
|
||||
.orderByDesc(BizAuditLogDO::getId));
|
||||
}
|
||||
|
||||
default List<BizAuditLogDO> selectListByBizType(String bizType, String actionType, LocalDateTime[] operateTime) {
|
||||
return selectList(new LambdaQueryWrapperX<BizAuditLogDO>()
|
||||
.eq(BizAuditLogDO::getBizType, bizType)
|
||||
.eqIfPresent(BizAuditLogDO::getActionType, actionType)
|
||||
.betweenIfPresent(BaseDO::getCreateTime, operateTime)
|
||||
.orderByDesc(BaseDO::getCreateTime)
|
||||
.orderByDesc(BizAuditLogDO::getId));
|
||||
}
|
||||
|
||||
default List<BizAuditLogDO> selectListByBizTypeAndActions(String bizType, List<String> actionTypes,
|
||||
LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return selectList(new LambdaQueryWrapperX<BizAuditLogDO>()
|
||||
.eq(BizAuditLogDO::getBizType, bizType)
|
||||
.inIfPresent(BizAuditLogDO::getActionType, actionTypes)
|
||||
.betweenIfPresent(BaseDO::getCreateTime, startTime, endTime)
|
||||
.orderByDesc(BaseDO::getCreateTime)
|
||||
.orderByDesc(BizAuditLogDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.member;
|
||||
|
||||
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.member.UserObjectRoleDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface UserObjectRoleMapper extends BaseMapperX<UserObjectRoleDO> {
|
||||
|
||||
default List<UserObjectRoleDO> selectListByObject(String objectType, Long objectId) {
|
||||
return selectList(new LambdaQueryWrapperX<UserObjectRoleDO>()
|
||||
.eq(UserObjectRoleDO::getObjectType, objectType)
|
||||
.eq(UserObjectRoleDO::getObjectId, objectId)
|
||||
.orderByAsc(UserObjectRoleDO::getStatus)
|
||||
.orderByAsc(UserObjectRoleDO::getJoinedTime)
|
||||
.orderByAsc(UserObjectRoleDO::getId));
|
||||
}
|
||||
|
||||
default UserObjectRoleDO selectByObjectAndUserId(String objectType, Long objectId, Long userId) {
|
||||
return selectOne(new LambdaQueryWrapperX<UserObjectRoleDO>()
|
||||
.eq(UserObjectRoleDO::getObjectType, objectType)
|
||||
.eq(UserObjectRoleDO::getObjectId, objectId)
|
||||
.eq(UserObjectRoleDO::getUserId, userId));
|
||||
}
|
||||
|
||||
default UserObjectRoleDO selectByIdAndObject(Long id, String objectType, Long objectId) {
|
||||
return selectOne(new LambdaQueryWrapperX<UserObjectRoleDO>()
|
||||
.eq(UserObjectRoleDO::getId, id)
|
||||
.eq(UserObjectRoleDO::getObjectType, objectType)
|
||||
.eq(UserObjectRoleDO::getObjectId, objectId));
|
||||
}
|
||||
|
||||
default UserObjectRoleDO selectActiveByObjectAndUserId(String objectType, Long objectId, Long userId) {
|
||||
return selectOne(new LambdaQueryWrapperX<UserObjectRoleDO>()
|
||||
.eq(UserObjectRoleDO::getObjectType, objectType)
|
||||
.eq(UserObjectRoleDO::getObjectId, objectId)
|
||||
.eq(UserObjectRoleDO::getUserId, userId)
|
||||
.eq(UserObjectRoleDO::getStatus, 0));
|
||||
}
|
||||
|
||||
default List<UserObjectRoleDO> selectListByIdsAndObject(List<Long> ids, String objectType, Long objectId) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return selectList(new LambdaQueryWrapperX<UserObjectRoleDO>()
|
||||
.in(UserObjectRoleDO::getId, ids)
|
||||
.eq(UserObjectRoleDO::getObjectType, objectType)
|
||||
.eq(UserObjectRoleDO::getObjectId, objectId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* MyBatis Mapper 包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.dal.mysql;
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.product;
|
||||
|
||||
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.product.vo.product.ProductPageReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ProductMapper extends BaseMapperX<ProductDO> {
|
||||
|
||||
default PageResult<ProductDO> selectPage(ProductPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ProductDO> queryWrapper = new LambdaQueryWrapperX<>();
|
||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
||||
queryWrapper.and(wrapper -> wrapper.like(ProductDO::getCode, reqVO.getKeyword())
|
||||
.or()
|
||||
.like(ProductDO::getName, reqVO.getKeyword()));
|
||||
}
|
||||
queryWrapper.eqIfPresent(ProductDO::getDirectionCode, reqVO.getDirectionCode())
|
||||
.eqIfPresent(ProductDO::getManagerUserId, reqVO.getManagerUserId())
|
||||
.eqIfPresent(ProductDO::getStatusCode, reqVO.getStatusCode())
|
||||
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime())
|
||||
.orderByDesc(BaseDO::getUpdateTime);
|
||||
return selectPage(reqVO, queryWrapper);
|
||||
}
|
||||
|
||||
default ProductDO selectByCode(String code) {
|
||||
return selectOne(ProductDO::getCode, code);
|
||||
}
|
||||
|
||||
default ProductDO selectByName(String name) {
|
||||
return selectOne(ProductDO::getName, name);
|
||||
}
|
||||
|
||||
default List<ProductDO> selectListByCodePrefix(String codePrefix) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductDO>()
|
||||
.likeRight(ProductDO::getCode, codePrefix)
|
||||
.orderByDesc(ProductDO::getCode));
|
||||
}
|
||||
|
||||
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
|
||||
ProductDO update = new ProductDO();
|
||||
update.setStatusCode(toStatus);
|
||||
update.setLastStatusReason(lastStatusReason);
|
||||
return update(update, new LambdaQueryWrapperX<ProductDO>()
|
||||
.eq(ProductDO::getId, id)
|
||||
.eq(ProductDO::getStatusCode, fromStatus));
|
||||
}
|
||||
|
||||
default int deleteByIdAndStatus(Long id, String statusCode) {
|
||||
return delete(new LambdaQueryWrapperX<ProductDO>()
|
||||
.eq(ProductDO::getId, id)
|
||||
.eq(ProductDO::getStatusCode, statusCode));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.product;
|
||||
|
||||
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.product.vo.requirement.ProductRequirementPageReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 产品需求 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProductRequirementMapper extends BaseMapperX<ProductRequirementDO> {
|
||||
|
||||
/**
|
||||
* 分页查询需求列表
|
||||
*/
|
||||
default PageResult<ProductRequirementDO> selectPage(ProductRequirementPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ProductRequirementDO> queryWrapper = buildQueryWrapper(reqVO);
|
||||
return selectPage(reqVO, queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有符合条件的需求列表(不分页,用于树形查询)
|
||||
*/
|
||||
default List<ProductRequirementDO> selectList(ProductRequirementPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ProductRequirementDO> queryWrapper = buildQueryWrapper(reqVO);
|
||||
return selectList(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*/
|
||||
private LambdaQueryWrapperX<ProductRequirementDO> buildQueryWrapper(ProductRequirementPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ProductRequirementDO> queryWrapper = new LambdaQueryWrapperX<>();
|
||||
// 标题模糊搜索
|
||||
if (StringUtils.hasText(reqVO.getTitle())) {
|
||||
queryWrapper.like(ProductRequirementDO::getTitle, reqVO.getTitle());
|
||||
}
|
||||
// 分类精确匹配
|
||||
queryWrapper.eqIfPresent(ProductRequirementDO::getCategory, reqVO.getCategory())
|
||||
// 优先级精确匹配
|
||||
.eqIfPresent(ProductRequirementDO::getPriority, reqVO.getPriority())
|
||||
// 状态精确匹配
|
||||
.eqIfPresent(ProductRequirementDO::getStatusCode, reqVO.getStatusCode())
|
||||
// 负责人精确匹配
|
||||
.eqIfPresent(ProductRequirementDO::getCurrentHandlerUserId, reqVO.getCurrentHandlerUserId())
|
||||
// 来源类型精确匹配
|
||||
.eqIfPresent(ProductRequirementDO::getSourceType, reqVO.getSourceType())
|
||||
// 模块ID列表IN查询(优先使用moduleIds,用于支持子模块查询)
|
||||
.inIfPresent(ProductRequirementDO::getModuleId, reqVO.getModuleIds())
|
||||
// 模块ID精确匹配(当moduleIds为空时使用)
|
||||
.eqIfPresent(ProductRequirementDO::getModuleId, reqVO.getModuleId())
|
||||
// 父需求ID精确匹配(查询子需求时使用)
|
||||
.eqIfPresent(ProductRequirementDO::getParentId, reqVO.getParentId())
|
||||
// 产品ID精确匹配
|
||||
.eq(ProductRequirementDO::getProductId, reqVO.getProductId())
|
||||
// 按排序值升序,再按创建时间降序
|
||||
.orderByAsc(ProductRequirementDO::getSort)
|
||||
.orderByDesc(ProductRequirementDO::getCreateTime);
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID和模块ID查询需求列表
|
||||
*/
|
||||
default List<ProductRequirementDO> selectListByProductIdAndModuleId(Long productId, Long moduleId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getProductId, productId)
|
||||
.eq(ProductRequirementDO::getModuleId, moduleId)
|
||||
.orderByAsc(ProductRequirementDO::getSort)
|
||||
.orderByDesc(ProductRequirementDO::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据父需求ID查询子需求列表
|
||||
*/
|
||||
default List<ProductRequirementDO> selectListByParentId(Long parentId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getParentId, parentId)
|
||||
.orderByAsc(ProductRequirementDO::getSort)
|
||||
.orderByDesc(ProductRequirementDO::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID查询所有需求(用于模块删除时级联处理)
|
||||
*/
|
||||
default List<ProductRequirementDO> selectListByProductId(Long productId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getProductId, productId)
|
||||
.orderByAsc(ProductRequirementDO::getSort));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模块ID查询需求列表
|
||||
*/
|
||||
default List<ProductRequirementDO> selectListByModuleId(Long moduleId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getModuleId, moduleId)
|
||||
.orderByAsc(ProductRequirementDO::getSort));
|
||||
}
|
||||
|
||||
/**
|
||||
* 带并发控制的状态更新(id + fromStatus 条件更新)
|
||||
*/
|
||||
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
|
||||
return updateStatusByIdAndStatusWithProject(id, fromStatus, toStatus, lastStatusReason, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带并发控制的状态更新(支持同时更新实现项目ID)
|
||||
*/
|
||||
default int updateStatusByIdAndStatusWithProject(Long id, String fromStatus, String toStatus, String lastStatusReason, Long implementProjectId) {
|
||||
ProductRequirementDO update = new ProductRequirementDO();
|
||||
update.setStatusCode(toStatus);
|
||||
update.setLastStatusReason(lastStatusReason);
|
||||
if (implementProjectId != null) {
|
||||
update.setImplementProjectId(implementProjectId);
|
||||
}
|
||||
return update(update, new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getId, id)
|
||||
.eq(ProductRequirementDO::getStatusCode, fromStatus));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID和状态删除(带并发控制)
|
||||
*/
|
||||
default int deleteByIdAndStatus(Long id, String statusCode) {
|
||||
return delete(new LambdaQueryWrapperX<ProductRequirementDO>()
|
||||
.eq(ProductRequirementDO::getId, id)
|
||||
.eq(ProductRequirementDO::getStatusCode, statusCode));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.product;
|
||||
|
||||
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.product.ProductRequirementModuleDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 产品需求模块 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProductRequirementModuleMapper extends BaseMapperX<ProductRequirementModuleDO> {
|
||||
|
||||
/**
|
||||
* 根据产品ID查询模块列表(用于构建模块树)
|
||||
*/
|
||||
default List<ProductRequirementModuleDO> selectListByProductId(Long productId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementModuleDO>()
|
||||
.eq(ProductRequirementModuleDO::getProductId, productId)
|
||||
.orderByAsc(ProductRequirementModuleDO::getSort)
|
||||
.orderByAsc(ProductRequirementModuleDO::getCreateTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据父模块ID查询子模块列表
|
||||
*/
|
||||
default List<ProductRequirementModuleDO> selectListByParentId(Long parentId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementModuleDO>()
|
||||
.eq(ProductRequirementModuleDO::getParentId, parentId)
|
||||
.orderByAsc(ProductRequirementModuleDO::getSort));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID和模块名称查询模块(用于校验名称唯一性)
|
||||
*/
|
||||
default ProductRequirementModuleDO selectByProductIdAndModuleName(Long productId, String moduleName) {
|
||||
return selectOne(new LambdaQueryWrapperX<ProductRequirementModuleDO>()
|
||||
.eq(ProductRequirementModuleDO::getProductId, productId)
|
||||
.eq(ProductRequirementModuleDO::getModuleName, moduleName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID和父模块ID查询模块(用于查找产品的"全部需求"根模块)
|
||||
*/
|
||||
default ProductRequirementModuleDO selectByProductIdAndParentId(Long productId, Long parentId) {
|
||||
return selectOne(new LambdaQueryWrapperX<ProductRequirementModuleDO>()
|
||||
.eq(ProductRequirementModuleDO::getProductId, productId)
|
||||
.eq(ProductRequirementModuleDO::getParentId, parentId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.product;
|
||||
|
||||
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.product.ProductRequirementStatusLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 产品需求状态变更日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProductRequirementStatusLogMapper extends BaseMapperX<ProductRequirementStatusLogDO> {
|
||||
|
||||
/**
|
||||
* 根据需求ID查询状态变更日志列表
|
||||
*/
|
||||
default List<ProductRequirementStatusLogDO> selectListByRequirementId(Long requirementId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductRequirementStatusLogDO>()
|
||||
.eq(ProductRequirementStatusLogDO::getRequirementId, requirementId)
|
||||
.orderByDesc(ProductRequirementStatusLogDO::getCreateTime));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.product;
|
||||
|
||||
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.product.ProductStatusLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ProductStatusLogMapper extends BaseMapperX<ProductStatusLogDO> {
|
||||
|
||||
default List<ProductStatusLogDO> selectListByProductId(Long productId, String actionType, LocalDateTime[] operateTime) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductStatusLogDO>()
|
||||
.eq(ProductStatusLogDO::getProductId, productId)
|
||||
.eqIfPresent(ProductStatusLogDO::getActionType, actionType)
|
||||
.betweenIfPresent(BaseDO::getCreateTime, operateTime)
|
||||
.orderByDesc(BaseDO::getCreateTime)
|
||||
.orderByDesc(ProductStatusLogDO::getId));
|
||||
}
|
||||
|
||||
default List<ProductStatusLogDO> selectListByProductIdAndActions(Long productId, List<String> actionTypes,
|
||||
LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductStatusLogDO>()
|
||||
.eq(ProductStatusLogDO::getProductId, productId)
|
||||
.inIfPresent(ProductStatusLogDO::getActionType, actionTypes)
|
||||
.betweenIfPresent(BaseDO::getCreateTime, startTime, endTime)
|
||||
.orderByDesc(BaseDO::getCreateTime)
|
||||
.orderByDesc(ProductStatusLogDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.status;
|
||||
|
||||
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.status.ObjectStatusModelDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ObjectStatusModelMapper extends BaseMapperX<ObjectStatusModelDO> {
|
||||
|
||||
default ObjectStatusModelDO selectByObjectTypeAndStatusCode(String objectType, String statusCode) {
|
||||
return selectOne(new LambdaQueryWrapperX<ObjectStatusModelDO>()
|
||||
.eq(ObjectStatusModelDO::getObjectType, objectType)
|
||||
.eq(ObjectStatusModelDO::getStatusCode, statusCode));
|
||||
}
|
||||
|
||||
default ObjectStatusModelDO selectByObjectTypeAndStatusCodeEnabled(String objectType, String statusCode) {
|
||||
return selectOne(new LambdaQueryWrapperX<ObjectStatusModelDO>()
|
||||
.eq(ObjectStatusModelDO::getObjectType, objectType)
|
||||
.eq(ObjectStatusModelDO::getStatusCode, statusCode)
|
||||
.eq(ObjectStatusModelDO::getStatus, 0));
|
||||
}
|
||||
|
||||
default List<ObjectStatusModelDO> selectListByObjectType(String objectType) {
|
||||
return selectList(new LambdaQueryWrapperX<ObjectStatusModelDO>()
|
||||
.eq(ObjectStatusModelDO::getObjectType, objectType)
|
||||
.orderByAsc(ObjectStatusModelDO::getSort));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.status;
|
||||
|
||||
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.status.ObjectStatusTransitionDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ObjectStatusTransitionMapper extends BaseMapperX<ObjectStatusTransitionDO> {
|
||||
|
||||
default ObjectStatusTransitionDO selectByObjectTypeAndFromStatusAndAction(String objectType,
|
||||
String fromStatusCode,
|
||||
String actionCode) {
|
||||
return selectOne(new LambdaQueryWrapperX<ObjectStatusTransitionDO>()
|
||||
.eq(ObjectStatusTransitionDO::getObjectType, objectType)
|
||||
.eq(ObjectStatusTransitionDO::getFromStatusCode, fromStatusCode)
|
||||
.eq(ObjectStatusTransitionDO::getActionCode, actionCode)
|
||||
.eq(ObjectStatusTransitionDO::getStatus, 0));
|
||||
}
|
||||
|
||||
default List<ObjectStatusTransitionDO> selectListByObjectTypeAndFromStatus(String objectType, String fromStatusCode) {
|
||||
return selectList(new LambdaQueryWrapperX<ObjectStatusTransitionDO>()
|
||||
.eq(ObjectStatusTransitionDO::getObjectType, objectType)
|
||||
.eq(ObjectStatusTransitionDO::getFromStatusCode, fromStatusCode)
|
||||
.eq(ObjectStatusTransitionDO::getStatus, 0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 持久层包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.dal;
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.njcn.rdms.module.project.framework.rpc.config;
|
||||
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Project 模块的 RPC 配置
|
||||
*/
|
||||
@Configuration(value = "projectRpcConfiguration", proxyBeanMethods = false)
|
||||
@EnableFeignClients(clients = {AdminUserApi.class, ObjectPermissionApi.class})
|
||||
public class RpcConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.njcn.rdms.module.project.framework.security.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 对象级权限校验注解。
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CheckObjectPermission {
|
||||
|
||||
/**
|
||||
* 对象类型,例如 product。
|
||||
*/
|
||||
String objectType();
|
||||
|
||||
/**
|
||||
* 对象编号 SpEL 表达式。
|
||||
*/
|
||||
String objectId();
|
||||
|
||||
/**
|
||||
* 对象权限码。
|
||||
*/
|
||||
String permission() default "";
|
||||
|
||||
/**
|
||||
* 是否仅校验成员身份。
|
||||
*/
|
||||
boolean memberOnly() default false;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.njcn.rdms.module.project.framework.security.aop;
|
||||
|
||||
import com.njcn.rdms.framework.common.util.spring.SpringExpressionUtils;
|
||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||
import com.njcn.rdms.module.project.framework.security.service.ObjectPermissionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
|
||||
/**
|
||||
* 对象级权限切面。
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ObjectPermissionAspect {
|
||||
|
||||
private final Map<String, ObjectPermissionService> objectPermissionServiceMap;
|
||||
|
||||
public ObjectPermissionAspect(List<ObjectPermissionService> objectPermissionServices) {
|
||||
this.objectPermissionServiceMap = objectPermissionServices.stream()
|
||||
.collect(Collectors.toMap(ObjectPermissionService::getObjectType, Function.identity()));
|
||||
}
|
||||
|
||||
@Around("@annotation(checkObjectPermission)")
|
||||
public Object aroundPointCut(ProceedingJoinPoint joinPoint,
|
||||
CheckObjectPermission checkObjectPermission) throws Throwable {
|
||||
ObjectPermissionService permissionService = objectPermissionServiceMap.get(checkObjectPermission.objectType());
|
||||
if (permissionService == null) {
|
||||
throw invalidParamException("暂不支持对象类型:{}", checkObjectPermission.objectType());
|
||||
}
|
||||
Long objectId = resolveObjectId(joinPoint, checkObjectPermission.objectId());
|
||||
permissionService.checkPermission(objectId, checkObjectPermission.permission(),
|
||||
checkObjectPermission.memberOnly());
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
private Long resolveObjectId(ProceedingJoinPoint joinPoint, String expression) {
|
||||
Object parsedValue = SpringExpressionUtils.parseExpression(joinPoint, expression);
|
||||
if (parsedValue instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (parsedValue instanceof String value && StringUtils.hasText(value)) {
|
||||
try {
|
||||
return Long.parseLong(value.trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
log.warn("[resolveObjectId][expression({}) value({}) 不是合法数字]", expression, value);
|
||||
}
|
||||
}
|
||||
if (Objects.isNull(parsedValue)) {
|
||||
throw invalidParamException("对象编号不能为空");
|
||||
}
|
||||
throw invalidParamException("对象编号解析失败:{}", expression);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.framework.security.config;
|
||||
|
||||
import com.njcn.rdms.framework.security.config.AuthorizeRequestsCustomizer;
|
||||
import com.njcn.rdms.module.project.enums.ApiConstants;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
|
||||
/**
|
||||
* Project 模块的 Security 配置
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false, value = "projectSecurityConfiguration")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Bean("projectAuthorizeRequestsCustomizer")
|
||||
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
|
||||
return new AuthorizeRequestsCustomizer() {
|
||||
|
||||
@Override
|
||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
// Swagger 接口文档
|
||||
registry.requestMatchers("/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/webjars/**").permitAll()
|
||||
.requestMatchers("/swagger-ui").permitAll()
|
||||
.requestMatchers("/swagger-ui/**").permitAll();
|
||||
// Druid 监控
|
||||
registry.requestMatchers("/druid/**").permitAll();
|
||||
// Spring Boot Actuator 的安全配置
|
||||
registry.requestMatchers("/actuator").permitAll()
|
||||
.requestMatchers("/actuator/**").permitAll();
|
||||
// RPC 服务的安全配置
|
||||
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.njcn.rdms.module.project.framework.security.service;
|
||||
|
||||
/**
|
||||
* 对象级权限服务。
|
||||
*/
|
||||
public interface ObjectPermissionService {
|
||||
|
||||
/**
|
||||
* 返回支持的对象类型。
|
||||
*/
|
||||
String getObjectType();
|
||||
|
||||
/**
|
||||
* 校验当前登录用户是否具备对象权限。
|
||||
*
|
||||
* @param objectId 对象编号
|
||||
* @param permission 权限码
|
||||
* @param memberOnly 是否只要求成员身份
|
||||
*/
|
||||
void checkPermission(Long objectId, String permission, boolean memberOnly);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.njcn.rdms.module.project.framework.security.service;
|
||||
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
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;
|
||||
|
||||
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 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
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
|
||||
@Override
|
||||
public String getObjectType() {
|
||||
return PRODUCT_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(PRODUCT_OBJECT_TYPE, objectId, loginUserId);
|
||||
if (currentMember == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED,
|
||||
buildDeniedPermission(permission, memberOnly));
|
||||
}
|
||||
if (memberOnly) {
|
||||
return;
|
||||
}
|
||||
String normalizedPermission = normalizePermission(permission);
|
||||
if (!getRolePermissions(currentMember.getRoleId()).contains(normalizedPermission)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED, normalizedPermission);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getRolePermissions(Long roleId) {
|
||||
Set<String> permissions = objectPermissionApi
|
||||
.getObjectRolePermissions(roleId, ROLE_SCOPE_OBJECT, PRODUCT_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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 服务层包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.service;
|
||||
@@ -0,0 +1,305 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
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.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;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO;
|
||||
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.ProductStatusLogMapper;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class 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;
|
||||
@Resource
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@Resource
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Resource
|
||||
private CacheManager cacheManager;
|
||||
@Resource
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
|
||||
public PageResult<ProductActivityRespVO> getProductActivities(Long productId, ProductActivityPageReqVO reqVO) {
|
||||
List<ActivityItem> items = new ArrayList<>();
|
||||
if (includeType(reqVO.getActivityType(), ObjectActivityConstants.ACTIVITY_TYPE_STATUS)) {
|
||||
productStatusLogMapper.selectListByProductId(productId, reqVO.getActionType(), reqVO.getOperateTime())
|
||||
.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())
|
||||
.forEach(log -> items.add(new ActivityItem(log.getId(), log.getCreateTime(), toProductActivity(log))));
|
||||
}
|
||||
if (includeType(reqVO.getActivityType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
|
||||
appendMemberActivities(productId, reqVO, items);
|
||||
}
|
||||
|
||||
items.sort(Comparator.comparing(ActivityItem::operateTime, Comparator.nullsLast(LocalDateTime::compareTo))
|
||||
.thenComparing(ActivityItem::sourceId, Comparator.nullsLast(Long::compareTo))
|
||||
.reversed());
|
||||
|
||||
List<ProductActivityRespVO> activities = items.stream()
|
||||
.map(ActivityItem::respVO)
|
||||
.toList();
|
||||
PageResult<ProductActivityRespVO> pageResult = buildPageResult(activities, reqVO);
|
||||
fillMemberRoleNames(pageResult.getList());
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
private void appendMemberActivities(Long productId, ProductActivityPageReqVO reqVO, List<ActivityItem> items) {
|
||||
List<BizAuditLogDO> memberLogs = bizAuditLogMapper
|
||||
.selectListByBizType(MEMBER_BIZ_TYPE, reqVO.getActionType(), reqVO.getOperateTime());
|
||||
if (memberLogs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Long> memberIds = memberLogs.stream()
|
||||
.map(BizAuditLogDO::getBizId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (memberIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Long, UserObjectRoleDO> memberMap = userObjectRoleMapper
|
||||
.selectListByIdsAndObject(memberIds, PRODUCT_OBJECT_TYPE, productId)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(UserObjectRoleDO::getId, Function.identity()));
|
||||
memberLogs.stream()
|
||||
.filter(log -> memberMap.containsKey(log.getBizId()))
|
||||
.forEach(log -> items.add(new ActivityItem(log.getId(), log.getCreateTime(), toMemberActivity(log))));
|
||||
}
|
||||
|
||||
private PageResult<ProductActivityRespVO> buildPageResult(List<ProductActivityRespVO> activities,
|
||||
ProductActivityPageReqVO reqVO) {
|
||||
if (activities.isEmpty()) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
int start = PageUtils.getStart(reqVO);
|
||||
if (start >= activities.size()) {
|
||||
return PageResult.empty((long) activities.size());
|
||||
}
|
||||
int end = Math.min(start + reqVO.getPageSize(), activities.size());
|
||||
return new PageResult<>(activities.subList(start, end), (long) activities.size());
|
||||
}
|
||||
|
||||
private void fillMemberRoleNames(List<ProductActivityRespVO> activities) {
|
||||
if (activities == null || activities.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> roleIds = new LinkedHashSet<>();
|
||||
for (ProductActivityRespVO activity : activities) {
|
||||
if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
|
||||
continue;
|
||||
}
|
||||
Long beforeRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "before");
|
||||
Long afterRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "after");
|
||||
if (beforeRoleId != null) {
|
||||
roleIds.add(beforeRoleId);
|
||||
}
|
||||
if (afterRoleId != null) {
|
||||
roleIds.add(afterRoleId);
|
||||
}
|
||||
}
|
||||
if (roleIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> roleNameMap = loadRoleNameMap(roleIds);
|
||||
for (ProductActivityRespVO activity : activities) {
|
||||
if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
|
||||
continue;
|
||||
}
|
||||
activity.setDetails(appendRoleNames(activity.getDetails(), roleNameMap));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, String> loadRoleNameMap(Set<Long> roleIds) {
|
||||
Map<Long, String> roleNameMap = new LinkedHashMap<>();
|
||||
if (roleIds == null || roleIds.isEmpty()) {
|
||||
return roleNameMap;
|
||||
}
|
||||
Cache cache = cacheManager == null ? null : cacheManager.getCache(ACTIVITY_ROLE_NAME_CACHE);
|
||||
Set<Long> missIds = new LinkedHashSet<>();
|
||||
for (Long roleId : roleIds) {
|
||||
String roleName = cache == null ? null : cache.get(roleId, String.class);
|
||||
if (roleName != null) {
|
||||
roleNameMap.put(roleId, roleName);
|
||||
} else {
|
||||
missIds.add(roleId);
|
||||
}
|
||||
}
|
||||
if (missIds.isEmpty()) {
|
||||
return roleNameMap;
|
||||
}
|
||||
Map<Long, ObjectRoleRespDTO> roleMap = objectPermissionApi == null ? Map.of()
|
||||
: objectPermissionApi.getObjectRoleMap(missIds, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE);
|
||||
if (roleMap == null || roleMap.isEmpty()) {
|
||||
return roleNameMap;
|
||||
}
|
||||
for (Long roleId : missIds) {
|
||||
ObjectRoleRespDTO role = roleMap.get(roleId);
|
||||
if (role == null || !StringUtils.hasText(role.getName())) {
|
||||
continue;
|
||||
}
|
||||
roleNameMap.put(roleId, role.getName());
|
||||
if (cache != null) {
|
||||
cache.put(roleId, role.getName());
|
||||
}
|
||||
}
|
||||
return roleNameMap;
|
||||
}
|
||||
|
||||
private Long getFieldChangeLong(String fieldChanges, String fieldName, String valueField) {
|
||||
JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
|
||||
if (valueNode == null || valueNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (valueNode.isNumber()) {
|
||||
return valueNode.longValue();
|
||||
}
|
||||
if (valueNode.isTextual() && StringUtils.hasText(valueNode.textValue())) {
|
||||
return Long.valueOf(valueNode.textValue().trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JsonNode getFieldChangeNode(String fieldChanges, String fieldName, String valueField) {
|
||||
if (!StringUtils.hasText(fieldChanges) || !JsonUtils.isJsonObject(fieldChanges)) {
|
||||
return null;
|
||||
}
|
||||
JsonNode fieldNode = JsonUtils.parseTree(fieldChanges).path(fieldName);
|
||||
if (fieldNode.isMissingNode()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode valueNode = fieldNode.path(valueField);
|
||||
return valueNode.isMissingNode() ? null : valueNode;
|
||||
}
|
||||
|
||||
private String appendRoleNames(String details, Map<Long, String> roleNameMap) {
|
||||
if (!StringUtils.hasText(details) || !JsonUtils.isJsonObject(details)) {
|
||||
return details;
|
||||
}
|
||||
Long beforeRoleId = getFieldChangeLong(details, "roleId", "before");
|
||||
Long afterRoleId = getFieldChangeLong(details, "roleId", "after");
|
||||
if (beforeRoleId == null && afterRoleId == null) {
|
||||
return details;
|
||||
}
|
||||
JsonNode detailsNode = JsonUtils.parseTree(details);
|
||||
if (!(detailsNode instanceof ObjectNode)) {
|
||||
return details;
|
||||
}
|
||||
ObjectNode objectNode = ((ObjectNode) detailsNode).deepCopy();
|
||||
ObjectNode roleNameNode = objectNode.putObject("roleName");
|
||||
appendRoleName(roleNameNode, "before", beforeRoleId, roleNameMap);
|
||||
appendRoleName(roleNameNode, "after", afterRoleId, roleNameMap);
|
||||
return JsonUtils.toJsonString(objectNode);
|
||||
}
|
||||
|
||||
private void appendRoleName(ObjectNode roleNameNode, String fieldName, Long roleId, Map<Long, String> roleNameMap) {
|
||||
if (roleId == null) {
|
||||
roleNameNode.putNull(fieldName);
|
||||
return;
|
||||
}
|
||||
String roleName = roleNameMap.get(roleId);
|
||||
if (StringUtils.hasText(roleName)) {
|
||||
roleNameNode.put(fieldName, roleName);
|
||||
return;
|
||||
}
|
||||
roleNameNode.putNull(fieldName);
|
||||
}
|
||||
|
||||
private boolean includeType(String actual, String expected) {
|
||||
return !StringUtils.hasText(actual) || Objects.equals(actual.trim(), expected);
|
||||
}
|
||||
|
||||
private ProductActivityRespVO toStatusActivity(ProductStatusLogDO log) {
|
||||
ProductActivityRespVO respVO = new ProductActivityRespVO();
|
||||
respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_STATUS);
|
||||
respVO.setActionType(log.getActionType());
|
||||
respVO.setActionName(ObjectActivityConstants.resolveActionName(log.getActionType()));
|
||||
respVO.setFromStatus(log.getFromStatus());
|
||||
respVO.setToStatus(log.getToStatus());
|
||||
respVO.setReason(log.getReason());
|
||||
respVO.setOperatorUserId(log.getOperatorUserId());
|
||||
respVO.setOperatorName(log.getOperatorName());
|
||||
respVO.setOperateTime(log.getCreateTime());
|
||||
respVO.setSummary(buildSummary(log.getOperatorName(), respVO.getActionName(), log.getReason()));
|
||||
respVO.setDetails(JsonUtils.toJsonString(buildStatusDetails(log)));
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildStatusDetails(ProductStatusLogDO log) {
|
||||
Map<String, Object> details = new LinkedHashMap<>();
|
||||
details.put("productCodeSnapshot", log.getProductCodeSnapshot());
|
||||
details.put("productNameSnapshot", log.getProductNameSnapshot());
|
||||
return details;
|
||||
}
|
||||
|
||||
private ProductActivityRespVO toProductActivity(BizAuditLogDO log) {
|
||||
ProductActivityRespVO respVO = new ProductActivityRespVO();
|
||||
respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_PRODUCT);
|
||||
respVO.setActionType(log.getActionType());
|
||||
respVO.setActionName(ObjectActivityConstants.resolveActionName(log.getActionType()));
|
||||
respVO.setFromStatus(log.getFromStatus());
|
||||
respVO.setToStatus(log.getToStatus());
|
||||
respVO.setReason(log.getReason());
|
||||
respVO.setOperatorUserId(log.getOperatorUserId());
|
||||
respVO.setOperatorName(log.getOperatorName());
|
||||
respVO.setOperateTime(log.getCreateTime());
|
||||
respVO.setSummary(buildSummary(log.getOperatorName(), respVO.getActionName(), log.getReason()));
|
||||
respVO.setDetails(log.getFieldChanges());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private ProductActivityRespVO toMemberActivity(BizAuditLogDO log) {
|
||||
ProductActivityRespVO respVO = toProductActivity(log);
|
||||
respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_MEMBER);
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private String buildSummary(String operatorName, String actionName, String reason) {
|
||||
String actualOperatorName = StringUtils.hasText(operatorName) ? operatorName : "系统";
|
||||
if (StringUtils.hasText(reason)) {
|
||||
return String.format("%s执行了【%s】:%s", actualOperatorName, actionName, reason);
|
||||
}
|
||||
return String.format("%s执行了【%s】", actualOperatorName, actionName);
|
||||
}
|
||||
|
||||
private record ActivityItem(Long sourceId, LocalDateTime operateTime, ProductActivityRespVO respVO) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
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.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;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO;
|
||||
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.ProductStatusLogMapper;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
|
||||
@Service
|
||||
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;
|
||||
@Resource
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@Resource
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Resource
|
||||
private CacheManager cacheManager;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
@Resource
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
|
||||
public PageResult<ProductActivityTimelineRespVO> getProductActivityTimelinePage(
|
||||
Long productId, ProductActivityTimelinePageReqVO reqVO) {
|
||||
LocalDateTime[] timeRange = buildTimeRange(reqVO);
|
||||
List<String> actionTypes = normalizeActionTypes(reqVO.getActionTypes());
|
||||
List<ActivityItem> items = new ArrayList<>();
|
||||
|
||||
appendStatusActivities(productId, reqVO.getActivityType(), actionTypes, timeRange, items);
|
||||
appendProductActivities(productId, reqVO.getActivityType(), actionTypes, timeRange, items);
|
||||
appendMemberActivities(productId, reqVO.getActivityType(), actionTypes, timeRange, items);
|
||||
|
||||
items.sort(Comparator.comparing(ActivityItem::occurredAt, Comparator.nullsLast(LocalDateTime::compareTo))
|
||||
.thenComparing(ActivityItem::sourceId, Comparator.nullsLast(Long::compareTo))
|
||||
.reversed());
|
||||
PageResult<ProductActivityTimelineRespVO> pageResult =
|
||||
buildPageResult(items.stream().map(ActivityItem::respVO).toList(), reqVO);
|
||||
fillTargetUserNames(pageResult.getList());
|
||||
fillMemberRoleNames(pageResult.getList());
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
private void appendStatusActivities(Long productId, String activityType, List<String> actionTypes,
|
||||
LocalDateTime[] timeRange, List<ActivityItem> items) {
|
||||
if (!includeType(activityType, ObjectActivityConstants.ACTIVITY_TYPE_STATUS)) {
|
||||
return;
|
||||
}
|
||||
List<String> statusActions = limitActions(actionTypes, ObjectActivityConstants.STATUS_ACTION_TYPES);
|
||||
if (shouldSkipByIntersection(actionTypes, statusActions)) {
|
||||
return;
|
||||
}
|
||||
productStatusLogMapper.selectListByProductIdAndActions(productId, statusActions, timeRange[0], timeRange[1])
|
||||
.forEach(log -> items.add(new ActivityItem(log.getId(), log.getCreateTime(), toStatusTimeline(log))));
|
||||
}
|
||||
|
||||
private void appendProductActivities(Long productId, String activityType, List<String> actionTypes,
|
||||
LocalDateTime[] timeRange, List<ActivityItem> items) {
|
||||
if (!includeType(activityType, ObjectActivityConstants.ACTIVITY_TYPE_PRODUCT)) {
|
||||
return;
|
||||
}
|
||||
List<String> productActions = limitActions(actionTypes, ObjectActivityConstants.PRODUCT_TIMELINE_ACTION_TYPES);
|
||||
if (shouldSkipByIntersection(actionTypes, productActions)) {
|
||||
return;
|
||||
}
|
||||
List<BizAuditLogDO> productLogs = bizAuditLogMapper.selectListByBizAndActions(
|
||||
ObjectActivityConstants.PRODUCT_BIZ_TYPE, productId, productActions, timeRange[0], timeRange[1]);
|
||||
Set<CreateSignature> createSignatures = buildCreateSignatures(productLogs);
|
||||
for (BizAuditLogDO log : productLogs) {
|
||||
if (ObjectActivityConstants.isStatusAction(log.getActionType())) {
|
||||
continue;
|
||||
}
|
||||
if (isCreateInitManagerNoise(log, createSignatures)) {
|
||||
continue;
|
||||
}
|
||||
if (!ObjectActivityConstants.PRODUCT_TIMELINE_ACTION_TYPES.contains(trim(log.getActionType()))) {
|
||||
continue;
|
||||
}
|
||||
items.add(new ActivityItem(log.getId(), log.getCreateTime(), toProductTimeline(log)));
|
||||
}
|
||||
}
|
||||
|
||||
private void appendMemberActivities(Long productId, String activityType, List<String> actionTypes,
|
||||
LocalDateTime[] timeRange, List<ActivityItem> items) {
|
||||
if (!includeType(activityType, ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
|
||||
return;
|
||||
}
|
||||
List<String> memberActions = limitActions(actionTypes, ObjectActivityConstants.MEMBER_TIMELINE_ACTION_TYPES);
|
||||
if (shouldSkipByIntersection(actionTypes, memberActions)) {
|
||||
return;
|
||||
}
|
||||
List<BizAuditLogDO> memberLogs = bizAuditLogMapper.selectListByBizTypeAndActions(
|
||||
ObjectActivityConstants.MEMBER_BIZ_TYPE, memberActions, timeRange[0], timeRange[1]);
|
||||
if (memberLogs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, UserObjectRoleDO> memberMap = loadMemberMap(productId, memberLogs);
|
||||
Set<CreateSignature> createSignatures = loadCreateSignatures(productId, timeRange);
|
||||
for (BizAuditLogDO log : memberLogs) {
|
||||
UserObjectRoleDO member = memberMap.get(log.getBizId());
|
||||
if (member == null) {
|
||||
continue;
|
||||
}
|
||||
if (!ObjectActivityConstants.MEMBER_TIMELINE_ACTION_TYPES.contains(trim(log.getActionType()))) {
|
||||
continue;
|
||||
}
|
||||
if (isCreateInitMemberNoise(log, createSignatures)) {
|
||||
continue;
|
||||
}
|
||||
items.add(new ActivityItem(log.getId(), log.getCreateTime(), toMemberTimeline(log, member)));
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDateTime[] buildTimeRange(ProductActivityTimelinePageReqVO reqVO) {
|
||||
if ((reqVO.getStartTime() == null) != (reqVO.getEndTime() == null)) {
|
||||
throw invalidParamException("开始时间和结束时间必须同时传入");
|
||||
}
|
||||
if (reqVO.getStartTime() == null) {
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
return new LocalDateTime[]{endTime.minusDays(30), endTime};
|
||||
}
|
||||
if (reqVO.getStartTime().isAfter(reqVO.getEndTime())) {
|
||||
throw invalidParamException("开始时间不能晚于结束时间");
|
||||
}
|
||||
return new LocalDateTime[]{reqVO.getStartTime(), reqVO.getEndTime()};
|
||||
}
|
||||
|
||||
private List<String> normalizeActionTypes(List<String> actionTypes) {
|
||||
if (actionTypes == null || actionTypes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<String> normalized = actionTypes.stream()
|
||||
.filter(StringUtils::hasText)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
private List<String> limitActions(List<String> actionTypes, List<String> allowedActions) {
|
||||
if (actionTypes == null || actionTypes.isEmpty()) {
|
||||
return allowedActions;
|
||||
}
|
||||
return actionTypes.stream()
|
||||
.filter(allowedActions::contains)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean shouldSkipByIntersection(List<String> actualActions, List<String> limitedActions) {
|
||||
return actualActions != null && actualActions.size() > 0 && limitedActions.isEmpty();
|
||||
}
|
||||
|
||||
private boolean includeType(String actual, String expected) {
|
||||
return !StringUtils.hasText(actual) || Objects.equals(actual.trim(), expected);
|
||||
}
|
||||
|
||||
private Map<Long, UserObjectRoleDO> loadMemberMap(Long productId, List<BizAuditLogDO> memberLogs) {
|
||||
List<Long> memberIds = memberLogs.stream()
|
||||
.map(BizAuditLogDO::getBizId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (memberIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<Long, UserObjectRoleDO> memberMap = new LinkedHashMap<>();
|
||||
for (UserObjectRoleDO member : userObjectRoleMapper.selectListByIdsAndObject(
|
||||
memberIds, ObjectActivityConstants.PRODUCT_BIZ_TYPE, productId)) {
|
||||
memberMap.put(member.getId(), member);
|
||||
}
|
||||
return memberMap;
|
||||
}
|
||||
|
||||
private Set<CreateSignature> loadCreateSignatures(Long productId, LocalDateTime[] timeRange) {
|
||||
List<BizAuditLogDO> productLogs = bizAuditLogMapper.selectListByBizAndActions(
|
||||
ObjectActivityConstants.PRODUCT_BIZ_TYPE, productId,
|
||||
ObjectActivityConstants.PRODUCT_TIMELINE_ACTION_TYPES, timeRange[0], timeRange[1]);
|
||||
return buildCreateSignatures(productLogs);
|
||||
}
|
||||
|
||||
private Set<CreateSignature> buildCreateSignatures(List<BizAuditLogDO> productLogs) {
|
||||
Set<CreateSignature> signatures = new LinkedHashSet<>();
|
||||
for (BizAuditLogDO log : productLogs) {
|
||||
if (!Objects.equals(trim(log.getActionType()), ObjectActivityConstants.PRODUCT_ACTION_CREATE)) {
|
||||
continue;
|
||||
}
|
||||
Long managerUserId = getFieldChangeLong(log.getFieldChanges(), "managerUserId", "after");
|
||||
if (managerUserId == null || log.getCreateTime() == null) {
|
||||
continue;
|
||||
}
|
||||
signatures.add(new CreateSignature(log.getCreateTime(), managerUserId));
|
||||
}
|
||||
return signatures;
|
||||
}
|
||||
|
||||
private boolean isCreateInitManagerNoise(BizAuditLogDO log, Set<CreateSignature> createSignatures) {
|
||||
if (!Objects.equals(trim(log.getActionType()), ObjectActivityConstants.PRODUCT_ACTION_CHANGE_MANAGER)) {
|
||||
return false;
|
||||
}
|
||||
if (getFieldChangeLong(log.getFieldChanges(), "managerUserId", "before") != null) {
|
||||
return false;
|
||||
}
|
||||
Long managerUserId = getFieldChangeLong(log.getFieldChanges(), "managerUserId", "after");
|
||||
return managerUserId != null && log.getCreateTime() != null
|
||||
&& createSignatures.contains(new CreateSignature(log.getCreateTime(), managerUserId));
|
||||
}
|
||||
|
||||
private boolean isCreateInitMemberNoise(BizAuditLogDO log, Set<CreateSignature> createSignatures) {
|
||||
if (!Objects.equals(trim(log.getActionType()), ObjectActivityConstants.MEMBER_ACTION_ADD)) {
|
||||
return false;
|
||||
}
|
||||
if (getFieldChangeLong(log.getFieldChanges(), "userId", "before") != null) {
|
||||
return false;
|
||||
}
|
||||
Long userId = getFieldChangeLong(log.getFieldChanges(), "userId", "after");
|
||||
return userId != null && log.getCreateTime() != null
|
||||
&& createSignatures.contains(new CreateSignature(log.getCreateTime(), userId));
|
||||
}
|
||||
|
||||
private Long getFieldChangeLong(String fieldChanges, String fieldName, String valueField) {
|
||||
JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
|
||||
if (valueNode == null || valueNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (valueNode.isNumber()) {
|
||||
return valueNode.longValue();
|
||||
}
|
||||
if (valueNode.isTextual() && StringUtils.hasText(valueNode.textValue())) {
|
||||
return Long.valueOf(valueNode.textValue().trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JsonNode getFieldChangeNode(String fieldChanges, String fieldName, String valueField) {
|
||||
if (!StringUtils.hasText(fieldChanges) || !JsonUtils.isJsonObject(fieldChanges)) {
|
||||
return null;
|
||||
}
|
||||
JsonNode fieldNode = JsonUtils.parseTree(fieldChanges).path(fieldName);
|
||||
if (fieldNode.isMissingNode()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode valueNode = fieldNode.path(valueField);
|
||||
return valueNode.isMissingNode() ? null : valueNode;
|
||||
}
|
||||
|
||||
private PageResult<ProductActivityTimelineRespVO> buildPageResult(List<ProductActivityTimelineRespVO> activities,
|
||||
ProductActivityTimelinePageReqVO reqVO) {
|
||||
if (activities.isEmpty()) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
int start = PageUtils.getStart(reqVO);
|
||||
if (start >= activities.size()) {
|
||||
return PageResult.empty((long) activities.size());
|
||||
}
|
||||
int end = Math.min(start + reqVO.getPageSize(), activities.size());
|
||||
return new PageResult<>(activities.subList(start, end), (long) activities.size());
|
||||
}
|
||||
|
||||
private void fillTargetUserNames(List<ProductActivityTimelineRespVO> activities) {
|
||||
if (activities == null || activities.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> userIds = new LinkedHashSet<>();
|
||||
for (ProductActivityTimelineRespVO activity : activities) {
|
||||
if (activity != null && activity.getTargetUserId() != null) {
|
||||
userIds.add(activity.getTargetUserId());
|
||||
}
|
||||
}
|
||||
if (userIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> nicknameMap = loadUserNicknameMap(userIds);
|
||||
for (ProductActivityTimelineRespVO activity : activities) {
|
||||
if (activity == null || activity.getTargetUserId() == null) {
|
||||
continue;
|
||||
}
|
||||
activity.setTargetUserName(nicknameMap.get(activity.getTargetUserId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillMemberRoleNames(List<ProductActivityTimelineRespVO> activities) {
|
||||
if (activities == null || activities.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> roleIds = new LinkedHashSet<>();
|
||||
for (ProductActivityTimelineRespVO activity : activities) {
|
||||
if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
|
||||
continue;
|
||||
}
|
||||
Long beforeRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "before");
|
||||
Long afterRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "after");
|
||||
if (beforeRoleId != null) {
|
||||
roleIds.add(beforeRoleId);
|
||||
}
|
||||
if (afterRoleId != null) {
|
||||
roleIds.add(afterRoleId);
|
||||
}
|
||||
}
|
||||
if (roleIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> roleNameMap = loadRoleNameMap(roleIds);
|
||||
for (ProductActivityTimelineRespVO activity : activities) {
|
||||
if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
|
||||
continue;
|
||||
}
|
||||
activity.setDetails(appendRoleNames(activity.getDetails(), roleNameMap));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, String> loadUserNicknameMap(Set<Long> userIds) {
|
||||
Map<Long, String> nicknameMap = new LinkedHashMap<>();
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return nicknameMap;
|
||||
}
|
||||
Cache cache = cacheManager == null ? null : cacheManager.getCache(TIMELINE_USER_NICKNAME_CACHE);
|
||||
Set<Long> missIds = new LinkedHashSet<>();
|
||||
for (Long userId : userIds) {
|
||||
String nickname = cache == null ? null : cache.get(userId, String.class);
|
||||
if (nickname != null) {
|
||||
nicknameMap.put(userId, nickname);
|
||||
} else {
|
||||
missIds.add(userId);
|
||||
}
|
||||
}
|
||||
if (missIds.isEmpty()) {
|
||||
return nicknameMap;
|
||||
}
|
||||
Map<Long, AdminUserRespDTO> userMap = adminUserApi == null ? Map.of() : adminUserApi.getUserMap(missIds);
|
||||
if (userMap == null || userMap.isEmpty()) {
|
||||
return nicknameMap;
|
||||
}
|
||||
for (Long userId : missIds) {
|
||||
AdminUserRespDTO user = userMap.get(userId);
|
||||
if (user == null) {
|
||||
continue;
|
||||
}
|
||||
nicknameMap.put(userId, user.getNickname());
|
||||
if (cache != null && user.getNickname() != null) {
|
||||
cache.put(userId, user.getNickname());
|
||||
}
|
||||
}
|
||||
return nicknameMap;
|
||||
}
|
||||
|
||||
private Map<Long, String> loadRoleNameMap(Set<Long> roleIds) {
|
||||
Map<Long, String> roleNameMap = new LinkedHashMap<>();
|
||||
if (roleIds == null || roleIds.isEmpty()) {
|
||||
return roleNameMap;
|
||||
}
|
||||
Cache cache = cacheManager == null ? null : cacheManager.getCache(TIMELINE_ROLE_NAME_CACHE);
|
||||
Set<Long> missIds = new LinkedHashSet<>();
|
||||
for (Long roleId : roleIds) {
|
||||
String roleName = cache == null ? null : cache.get(roleId, String.class);
|
||||
if (roleName != null) {
|
||||
roleNameMap.put(roleId, roleName);
|
||||
} else {
|
||||
missIds.add(roleId);
|
||||
}
|
||||
}
|
||||
if (missIds.isEmpty()) {
|
||||
return roleNameMap;
|
||||
}
|
||||
Map<Long, ObjectRoleRespDTO> roleMap = objectPermissionApi == null ? Map.of()
|
||||
: objectPermissionApi.getObjectRoleMap(missIds, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE);
|
||||
if (roleMap == null || roleMap.isEmpty()) {
|
||||
return roleNameMap;
|
||||
}
|
||||
for (Long roleId : missIds) {
|
||||
ObjectRoleRespDTO role = roleMap.get(roleId);
|
||||
if (role == null || !StringUtils.hasText(role.getName())) {
|
||||
continue;
|
||||
}
|
||||
roleNameMap.put(roleId, role.getName());
|
||||
if (cache != null) {
|
||||
cache.put(roleId, role.getName());
|
||||
}
|
||||
}
|
||||
return roleNameMap;
|
||||
}
|
||||
|
||||
private String appendRoleNames(String details, Map<Long, String> roleNameMap) {
|
||||
if (!StringUtils.hasText(details) || !JsonUtils.isJsonObject(details)) {
|
||||
return details;
|
||||
}
|
||||
Long beforeRoleId = getFieldChangeLong(details, "roleId", "before");
|
||||
Long afterRoleId = getFieldChangeLong(details, "roleId", "after");
|
||||
if (beforeRoleId == null && afterRoleId == null) {
|
||||
return details;
|
||||
}
|
||||
JsonNode detailsNode = JsonUtils.parseTree(details);
|
||||
if (!(detailsNode instanceof ObjectNode)) {
|
||||
return details;
|
||||
}
|
||||
ObjectNode objectNode = ((ObjectNode) detailsNode).deepCopy();
|
||||
ObjectNode roleNameNode = objectNode.putObject("roleName");
|
||||
appendRoleName(roleNameNode, "before", beforeRoleId, roleNameMap);
|
||||
appendRoleName(roleNameNode, "after", afterRoleId, roleNameMap);
|
||||
return JsonUtils.toJsonString(objectNode);
|
||||
}
|
||||
|
||||
private void appendRoleName(ObjectNode roleNameNode, String fieldName, Long roleId, Map<Long, String> roleNameMap) {
|
||||
if (roleId == null) {
|
||||
roleNameNode.putNull(fieldName);
|
||||
return;
|
||||
}
|
||||
String roleName = roleNameMap.get(roleId);
|
||||
if (StringUtils.hasText(roleName)) {
|
||||
roleNameNode.put(fieldName, roleName);
|
||||
return;
|
||||
}
|
||||
roleNameNode.putNull(fieldName);
|
||||
}
|
||||
|
||||
private ProductActivityTimelineRespVO toStatusTimeline(ProductStatusLogDO log) {
|
||||
ProductActivityTimelineRespVO respVO = new ProductActivityTimelineRespVO();
|
||||
respVO.setId(ObjectActivityConstants.ACTIVITY_TYPE_STATUS + ":" + log.getId());
|
||||
respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_STATUS);
|
||||
respVO.setActionType(log.getActionType());
|
||||
respVO.setActionName(ObjectActivityConstants.resolveActionName(log.getActionType()));
|
||||
respVO.setOperatorUserId(log.getOperatorUserId());
|
||||
respVO.setOperatorName(log.getOperatorName());
|
||||
respVO.setOccurredAt(log.getCreateTime());
|
||||
respVO.setSummary(buildSummary(log.getOperatorName(), respVO.getActionName(), log.getReason()));
|
||||
respVO.setReason(log.getReason());
|
||||
respVO.setFromStatus(log.getFromStatus());
|
||||
respVO.setToStatus(log.getToStatus());
|
||||
respVO.setDetails(JsonUtils.toJsonString(buildStatusDetails(log)));
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private ProductActivityTimelineRespVO toProductTimeline(BizAuditLogDO log) {
|
||||
ProductActivityTimelineRespVO respVO = new ProductActivityTimelineRespVO();
|
||||
respVO.setId(ObjectActivityConstants.ACTIVITY_TYPE_PRODUCT + ":" + log.getId());
|
||||
respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_PRODUCT);
|
||||
respVO.setActionType(log.getActionType());
|
||||
respVO.setActionName(ObjectActivityConstants.resolveActionName(log.getActionType()));
|
||||
respVO.setOperatorUserId(log.getOperatorUserId());
|
||||
respVO.setOperatorName(log.getOperatorName());
|
||||
respVO.setOccurredAt(log.getCreateTime());
|
||||
respVO.setSummary(buildSummary(log.getOperatorName(), respVO.getActionName(), log.getReason()));
|
||||
respVO.setReason(log.getReason());
|
||||
respVO.setFromStatus(log.getFromStatus());
|
||||
respVO.setToStatus(log.getToStatus());
|
||||
respVO.setDetails(log.getFieldChanges());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private ProductActivityTimelineRespVO toMemberTimeline(BizAuditLogDO log, UserObjectRoleDO member) {
|
||||
ProductActivityTimelineRespVO respVO = toProductTimeline(log);
|
||||
respVO.setId(ObjectActivityConstants.ACTIVITY_TYPE_MEMBER + ":" + log.getId());
|
||||
respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_MEMBER);
|
||||
respVO.setTargetUserId(member == null ? null : member.getUserId());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildStatusDetails(ProductStatusLogDO log) {
|
||||
Map<String, Object> details = new LinkedHashMap<>();
|
||||
details.put("productCodeSnapshot", log.getProductCodeSnapshot());
|
||||
details.put("productNameSnapshot", log.getProductNameSnapshot());
|
||||
return details;
|
||||
}
|
||||
|
||||
private String buildSummary(String operatorName, String actionName, String reason) {
|
||||
String actualOperatorName = StringUtils.hasText(operatorName) ? operatorName : "系统";
|
||||
if (StringUtils.hasText(reason)) {
|
||||
return String.format("%s执行了【%s】:%s", actualOperatorName, actionName, reason);
|
||||
}
|
||||
return String.format("%s执行了【%s】", actualOperatorName, actionName);
|
||||
}
|
||||
|
||||
private String trim(String value) {
|
||||
return StringUtils.hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
private record CreateSignature(LocalDateTime occurredAt, Long userId) {
|
||||
}
|
||||
|
||||
private record ActivityItem(Long sourceId, LocalDateTime occurredAt, ProductActivityTimelineRespVO respVO) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
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;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberUpdateReqVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 产品团队 Service 接口
|
||||
*/
|
||||
public interface ProductMemberService {
|
||||
|
||||
/**
|
||||
* 获取产品团队成员列表
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 成员列表
|
||||
*/
|
||||
List<ProductMemberRespVO> getProductMemberList(Long productId);
|
||||
|
||||
/**
|
||||
* 新增产品团队成员
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @param reqVO 请求参数
|
||||
* @return 团队关系编号
|
||||
*/
|
||||
Long createProductMember(Long productId, ProductMemberSaveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 调整产品团队成员角色
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @param memberId 成员关系编号
|
||||
* @param reqVO 请求参数
|
||||
*/
|
||||
void updateProductMember(Long productId, Long memberId, ProductMemberUpdateReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 移出产品团队成员
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @param memberId 成员关系编号
|
||||
* @param reqVO 请求参数
|
||||
*/
|
||||
void inactiveProductMember(Long productId, Long memberId, ProductMemberInactiveReqVO reqVO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
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.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;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||
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.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.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
/**
|
||||
* 产品团队 Service 实现类
|
||||
*/
|
||||
@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
|
||||
private UserObjectRoleMapper userObjectRoleMapper;
|
||||
@Resource
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Resource
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@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);
|
||||
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 -> {
|
||||
ProductMemberRespVO respVO = new ProductMemberRespVO();
|
||||
respVO.setId(member.getId());
|
||||
respVO.setUserId(member.getUserId());
|
||||
AdminUserRespDTO user = userMap.get(member.getUserId());
|
||||
respVO.setUserNickname(user == null ? null : user.getNickname());
|
||||
respVO.setRoleId(member.getRoleId());
|
||||
ObjectRoleRespDTO role = roleMap.get(member.getRoleId());
|
||||
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));
|
||||
respVO.setStatus(member.getStatus());
|
||||
respVO.setJoinedTime(member.getJoinedTime());
|
||||
respVO.setLeftTime(member.getLeftTime());
|
||||
respVO.setRemark(member.getRemark());
|
||||
return respVO;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_UPDATE_PERMISSION)
|
||||
public Long createProductMember(Long productId, ProductMemberSaveReqVO reqVO) {
|
||||
ProductDO product = validateProductExists(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)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
UserObjectRoleDO member;
|
||||
UserObjectRoleDO before = null;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (existingMember == null) {
|
||||
member = new UserObjectRoleDO();
|
||||
member.setUserId(reqVO.getUserId());
|
||||
member.setObjectType(PRODUCT_OBJECT_TYPE);
|
||||
member.setObjectId(productId);
|
||||
member.setRoleId(targetRole.getId());
|
||||
member.setStatus(MEMBER_STATUS_ACTIVE);
|
||||
member.setJoinedTime(now);
|
||||
member.setLeftTime(null);
|
||||
member.setRemark(normalizeNullableText(reqVO.getRemark()));
|
||||
userObjectRoleMapper.insert(member);
|
||||
} else {
|
||||
before = cloneMember(existingMember);
|
||||
member = existingMember;
|
||||
member.setRoleId(targetRole.getId());
|
||||
member.setStatus(MEMBER_STATUS_ACTIVE);
|
||||
member.setJoinedTime(now);
|
||||
member.setLeftTime(null);
|
||||
member.setRemark(normalizeNullableText(reqVO.getRemark()));
|
||||
userObjectRoleMapper.updateById(member);
|
||||
}
|
||||
|
||||
writeMemberAuditLog(member, ObjectActivityConstants.MEMBER_ACTION_ADD, before, member, null);
|
||||
if (isManagerRole(targetRole)) {
|
||||
transferManager(product, member, reqVO.getPreviousManagerUserId(), reqVO.getPreviousManagerRoleId(), null);
|
||||
}
|
||||
return member.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_UPDATE_PERMISSION)
|
||||
public void updateProductMember(Long productId, Long memberId, ProductMemberUpdateReqVO reqVO) {
|
||||
ProductDO product = validateProductExists(productId);
|
||||
UserObjectRoleDO member = validateMemberExists(productId, memberId);
|
||||
if (!Objects.equals(member.getStatus(), MEMBER_STATUS_ACTIVE)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_NOT_ACTIVE);
|
||||
}
|
||||
|
||||
ObjectRoleRespDTO targetRole = validateProductRole(reqVO.getRoleId());
|
||||
UserObjectRoleDO before = cloneMember(member);
|
||||
member.setRemark(normalizeNullableText(reqVO.getRemark()));
|
||||
|
||||
if (isManagerRole(targetRole)) {
|
||||
member.setRoleId(targetRole.getId());
|
||||
userObjectRoleMapper.updateById(member);
|
||||
transferManager(product, member, reqVO.getPreviousManagerUserId(),
|
||||
reqVO.getPreviousManagerRoleId(), normalizeNullableText(reqVO.getReason()));
|
||||
} else {
|
||||
if (Objects.equals(member.getUserId(), product.getManagerUserId())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MANAGER_MEMBER_NOT_ALLOW_DOWNGRADE);
|
||||
}
|
||||
member.setRoleId(targetRole.getId());
|
||||
userObjectRoleMapper.updateById(member);
|
||||
}
|
||||
|
||||
writeMemberAuditLog(member, ObjectActivityConstants.MEMBER_ACTION_UPDATE,
|
||||
before, member, normalizeNullableText(reqVO.getReason()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_UPDATE_PERMISSION)
|
||||
public void inactiveProductMember(Long productId, Long memberId, ProductMemberInactiveReqVO reqVO) {
|
||||
ProductDO product = validateProductExists(productId);
|
||||
UserObjectRoleDO member = validateMemberExists(productId, memberId);
|
||||
if (!Objects.equals(member.getStatus(), MEMBER_STATUS_ACTIVE)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_NOT_ACTIVE);
|
||||
}
|
||||
if (Objects.equals(member.getUserId(), product.getManagerUserId())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MANAGER_MEMBER_NOT_ALLOW_REMOVE);
|
||||
}
|
||||
|
||||
UserObjectRoleDO before = cloneMember(member);
|
||||
member.setStatus(MEMBER_STATUS_INACTIVE);
|
||||
member.setLeftTime(LocalDateTime.now());
|
||||
userObjectRoleMapper.updateById(member);
|
||||
|
||||
writeMemberAuditLog(member, ObjectActivityConstants.MEMBER_ACTION_REMOVE, before, member,
|
||||
normalizeNullableText(reqVO.getReason()));
|
||||
}
|
||||
|
||||
private ProductDO validateProductExists(Long productId) {
|
||||
if (productId == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
|
||||
}
|
||||
ProductDO product = productMapper.selectById(productId);
|
||||
if (product == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
private UserObjectRoleDO validateMemberExists(Long productId, Long memberId) {
|
||||
UserObjectRoleDO member = userObjectRoleMapper.selectByIdAndObject(memberId, PRODUCT_OBJECT_TYPE, productId);
|
||||
if (member == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_NOT_EXISTS);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
private ObjectRoleRespDTO validateProductRole(Long roleId) {
|
||||
ObjectRoleRespDTO role = objectPermissionApi
|
||||
.getObjectRoleById(roleId, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
|
||||
.getCheckedData();
|
||||
if (role == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_ROLE_INVALID);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
private void transferManager(ProductDO product,
|
||||
UserObjectRoleDO targetManagerMember,
|
||||
Long previousManagerUserId,
|
||||
Long previousManagerRoleId,
|
||||
String reason) {
|
||||
Long currentManagerUserId = product.getManagerUserId();
|
||||
Long targetManagerUserId = targetManagerMember.getUserId();
|
||||
if (Objects.equals(currentManagerUserId, targetManagerUserId)) {
|
||||
product.setManagerUserId(targetManagerUserId);
|
||||
productMapper.updateById(product);
|
||||
return;
|
||||
}
|
||||
|
||||
ObjectRoleRespDTO previousManagerRole = validatePreviousManagerTransfer(currentManagerUserId,
|
||||
previousManagerUserId, previousManagerRoleId);
|
||||
transferPreviousManager(product.getId(), previousManagerUserId, previousManagerRole.getId(), reason);
|
||||
|
||||
product.setManagerUserId(targetManagerUserId);
|
||||
productMapper.updateById(product);
|
||||
writeManagerChangeAuditLog(product.getId(), currentManagerUserId, targetManagerUserId, reason);
|
||||
}
|
||||
|
||||
private ObjectRoleRespDTO validatePreviousManagerTransfer(Long currentManagerUserId,
|
||||
Long previousManagerUserId,
|
||||
Long previousManagerRoleId) {
|
||||
if (currentManagerUserId == null
|
||||
|| previousManagerUserId == null
|
||||
|| previousManagerRoleId == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MANAGER_TRANSFER_INFO_REQUIRED);
|
||||
}
|
||||
if (!Objects.equals(currentManagerUserId, previousManagerUserId)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MANAGER_TRANSFER_SOURCE_INVALID);
|
||||
}
|
||||
ObjectRoleRespDTO previousManagerRole = validateProductRole(previousManagerRoleId);
|
||||
if (isManagerRole(previousManagerRole)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MANAGER_TRANSFER_ROLE_INVALID);
|
||||
}
|
||||
return previousManagerRole;
|
||||
}
|
||||
|
||||
private void transferPreviousManager(Long productId, Long previousManagerUserId, Long previousManagerRoleId, String reason) {
|
||||
UserObjectRoleDO existingMember = userObjectRoleMapper
|
||||
.selectByObjectAndUserId(PRODUCT_OBJECT_TYPE, productId, previousManagerUserId);
|
||||
UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
UserObjectRoleDO member;
|
||||
String actionType;
|
||||
if (existingMember == null) {
|
||||
member = new UserObjectRoleDO();
|
||||
member.setUserId(previousManagerUserId);
|
||||
member.setObjectType(PRODUCT_OBJECT_TYPE);
|
||||
member.setObjectId(productId);
|
||||
member.setRoleId(previousManagerRoleId);
|
||||
member.setStatus(MEMBER_STATUS_ACTIVE);
|
||||
member.setJoinedTime(now);
|
||||
member.setLeftTime(null);
|
||||
member.setRemark(null);
|
||||
userObjectRoleMapper.insert(member);
|
||||
actionType = ObjectActivityConstants.MEMBER_ACTION_ADD;
|
||||
} else {
|
||||
member = existingMember;
|
||||
member.setRoleId(previousManagerRoleId);
|
||||
member.setStatus(MEMBER_STATUS_ACTIVE);
|
||||
member.setLeftTime(null);
|
||||
if (!Objects.equals(before.getStatus(), MEMBER_STATUS_ACTIVE)) {
|
||||
member.setJoinedTime(now);
|
||||
actionType = ObjectActivityConstants.MEMBER_ACTION_ADD;
|
||||
} else {
|
||||
actionType = ObjectActivityConstants.MEMBER_ACTION_UPDATE;
|
||||
}
|
||||
userObjectRoleMapper.updateById(member);
|
||||
}
|
||||
writeMemberAuditLog(member, actionType, before, member, reason);
|
||||
}
|
||||
|
||||
private boolean isManagerRole(ObjectRoleRespDTO role) {
|
||||
return Objects.equals(PRODUCT_MANAGER_ROLE_CODE, role.getCode());
|
||||
}
|
||||
|
||||
private Map<Long, ObjectRoleRespDTO> getRoleMap(Set<Long> roleIds) {
|
||||
if (roleIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ObjectRoleRespDTO> roles = objectPermissionApi
|
||||
.getObjectRoleList(roleIds, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
|
||||
.getCheckedData();
|
||||
if (roles == null || roles.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return roles.stream().collect(Collectors.toMap(ObjectRoleRespDTO::getId, Function.identity()));
|
||||
}
|
||||
|
||||
private Map<Long, AdminUserRespDTO> getUserMap(Set<Long> userIds) {
|
||||
if (userIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return adminUserApi.getUserMap(userIds);
|
||||
}
|
||||
|
||||
private void writeMemberAuditLog(UserObjectRoleDO member,
|
||||
String actionType,
|
||||
UserObjectRoleDO before,
|
||||
UserObjectRoleDO after,
|
||||
String reason) {
|
||||
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||
auditLog.setBizType(ObjectActivityConstants.MEMBER_BIZ_TYPE);
|
||||
auditLog.setBizId(member.getId());
|
||||
auditLog.setActionType(actionType);
|
||||
auditLog.setFieldChanges(buildMemberFieldChanges(before, after));
|
||||
auditLog.setReason(reason);
|
||||
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
private void writeManagerChangeAuditLog(Long productId, Long beforeManagerUserId, Long afterManagerUserId, String reason) {
|
||||
if (Objects.equals(beforeManagerUserId, afterManagerUserId)) {
|
||||
return;
|
||||
}
|
||||
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||
auditLog.setBizType(PRODUCT_OBJECT_TYPE);
|
||||
auditLog.setBizId(productId);
|
||||
auditLog.setActionType(ObjectActivityConstants.PRODUCT_ACTION_CHANGE_MANAGER);
|
||||
auditLog.setFieldChanges(buildManagerFieldChanges(beforeManagerUserId, afterManagerUserId));
|
||||
auditLog.setReason(reason);
|
||||
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
private String buildMemberFieldChanges(UserObjectRoleDO before, UserObjectRoleDO after) {
|
||||
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||
appendFieldChange(fieldChanges, "userId", valueOf(before, UserObjectRoleDO::getUserId),
|
||||
valueOf(after, UserObjectRoleDO::getUserId));
|
||||
appendFieldChange(fieldChanges, "roleId", valueOf(before, UserObjectRoleDO::getRoleId),
|
||||
valueOf(after, UserObjectRoleDO::getRoleId));
|
||||
appendFieldChange(fieldChanges, "status", valueOf(before, UserObjectRoleDO::getStatus),
|
||||
valueOf(after, UserObjectRoleDO::getStatus));
|
||||
appendFieldChange(fieldChanges, "joinedTime", valueOf(before, UserObjectRoleDO::getJoinedTime),
|
||||
valueOf(after, UserObjectRoleDO::getJoinedTime));
|
||||
appendFieldChange(fieldChanges, "leftTime", valueOf(before, UserObjectRoleDO::getLeftTime),
|
||||
valueOf(after, UserObjectRoleDO::getLeftTime));
|
||||
appendFieldChange(fieldChanges, "remark", valueOf(before, UserObjectRoleDO::getRemark),
|
||||
valueOf(after, UserObjectRoleDO::getRemark));
|
||||
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
|
||||
}
|
||||
|
||||
private String buildManagerFieldChanges(Long beforeManagerUserId, Long afterManagerUserId) {
|
||||
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||
appendFieldChange(fieldChanges, "managerUserId", beforeManagerUserId, afterManagerUserId);
|
||||
return JsonUtils.toJsonString(fieldChanges);
|
||||
}
|
||||
|
||||
private UserObjectRoleDO cloneMember(UserObjectRoleDO source) {
|
||||
UserObjectRoleDO clone = new UserObjectRoleDO();
|
||||
clone.setId(source.getId());
|
||||
clone.setUserId(source.getUserId());
|
||||
clone.setObjectType(source.getObjectType());
|
||||
clone.setObjectId(source.getObjectId());
|
||||
clone.setRoleId(source.getRoleId());
|
||||
clone.setStatus(source.getStatus());
|
||||
clone.setJoinedTime(source.getJoinedTime());
|
||||
clone.setLeftTime(source.getLeftTime());
|
||||
clone.setRemark(source.getRemark());
|
||||
return clone;
|
||||
}
|
||||
|
||||
private <T> T valueOf(UserObjectRoleDO member, Function<UserObjectRoleDO, T> getter) {
|
||||
return member == null ? null : getter.apply(member);
|
||||
}
|
||||
|
||||
private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
|
||||
if (Objects.equals(before, after)) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> value = new LinkedHashMap<>();
|
||||
value.put("before", before);
|
||||
value.put("after", after);
|
||||
fieldChanges.put(fieldName, value);
|
||||
}
|
||||
|
||||
private String normalizeNullableText(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String defaultText(String value) {
|
||||
return StringUtils.hasText(value) ? value : "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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.requirement.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 产品需求 Service 接口
|
||||
*/
|
||||
public interface ProductRequirementService {
|
||||
|
||||
/**
|
||||
* 创建产品需求
|
||||
*
|
||||
* @param createReqVO 创建请求
|
||||
* @return 需求编号
|
||||
*/
|
||||
Long createRequirement(ProductRequirementSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新产品需求(不含状态变更)
|
||||
*
|
||||
* @param updateReqVO 更新请求
|
||||
*/
|
||||
void updateRequirement(ProductRequirementUpdateReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 获取需求详情
|
||||
*
|
||||
* @param id 需求编号
|
||||
* @return 需求详情
|
||||
*/
|
||||
ProductRequirementRespVO getRequirement(Long id, Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求分页列表
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获取需求树形列表(分页)
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页结果(只按父需求分页,子需求不计入分页)
|
||||
*/
|
||||
PageResult<ProductRequirementRespVO> getRequirementTree(ProductRequirementPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 变更需求状态
|
||||
*
|
||||
* @param reqVO 状态动作请求
|
||||
*/
|
||||
void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除需求
|
||||
*
|
||||
* @param id 需求编号
|
||||
* @param productId 产品编号
|
||||
*/
|
||||
void deleteRequirement(Long id, Long productId);
|
||||
|
||||
/**
|
||||
* 拆分需求(创建子需求)
|
||||
*
|
||||
* @param reqVO 拆分请求
|
||||
* @return 子需求编号
|
||||
*/
|
||||
Long splitRequirement(ProductRequirementSplitReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 关闭需求(大需求关闭时级联关闭子需求)
|
||||
*
|
||||
* @param reqVO 关闭请求
|
||||
*/
|
||||
void closeRequirement(ProductRequirementCloseReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 获取需求当前可执行的状态动作列表
|
||||
*
|
||||
* @param requirementId 需求编号
|
||||
* @param productId 产品编号
|
||||
* @return 可执行动作列表
|
||||
*/
|
||||
List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求生命周期信息(当前状态 + 可执行动作)
|
||||
*
|
||||
* @param requirementId 需求编号
|
||||
* @param productId 产品编号
|
||||
* @return 生命周期信息
|
||||
*/
|
||||
ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId);
|
||||
|
||||
// ========== 模块管理 ==========
|
||||
|
||||
/**
|
||||
* 创建需求模块
|
||||
*
|
||||
* @param reqVO 模块保存请求
|
||||
* @return 模块编号
|
||||
*/
|
||||
Long createRequirementModule(ProductRequirementModuleReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 更新需求模块
|
||||
*
|
||||
* @param reqVO 模块保存请求
|
||||
*/
|
||||
void updateRequirementModule(ProductRequirementModuleReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除需求模块(级联删除模块下需求)
|
||||
*
|
||||
* @param moduleId 模块编号
|
||||
* @param productId 产品编号
|
||||
*/
|
||||
void deleteRequirementModule(Long moduleId, Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求模块树
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 模块树
|
||||
*/
|
||||
List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId);
|
||||
|
||||
/**
|
||||
* 获取需求所有状态字典列表
|
||||
*
|
||||
* @return 状态字典列表
|
||||
*/
|
||||
List<ProductRequirementStatusDictRespVO> getRequirementStatusDict();
|
||||
|
||||
/**
|
||||
* 获取需求终止态状态字典列表
|
||||
*
|
||||
* @return 终止态状态字典列表
|
||||
*/
|
||||
List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,914 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
|
||||
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.product.ProductRequirementMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper;
|
||||
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;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
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 实现类
|
||||
*/
|
||||
@Service
|
||||
public class ProductRequirementServiceImpl implements ProductRequirementService {
|
||||
|
||||
// 对象类型常量
|
||||
private static final String REQUIREMENT_OBJECT_TYPE = "product_requirement";
|
||||
private static final String PRODUCT_OBJECT_TYPE = "product";
|
||||
|
||||
// 需求状态常量
|
||||
private static final String STATUS_PENDING_CONFIRM = "pending_confirm";
|
||||
private static final String STATUS_PENDING_REVIEW = "pending_review";
|
||||
private static final String STATUS_PENDING_DISPATCH = "pending_dispatch";
|
||||
private static final String STATUS_IMPLEMENTING = "implementing";
|
||||
private static final String STATUS_ACCEPTED = "accepted";
|
||||
private static final String STATUS_CLOSED = "closed";
|
||||
private static final String STATUS_REJECTED = "rejected";
|
||||
private static final String STATUS_CANCELLED = "cancelled";
|
||||
|
||||
// 终态状态集合
|
||||
private static final List<String> TERMINAL_STATUSES = List.of(STATUS_CLOSED, STATUS_REJECTED, STATUS_CANCELLED);
|
||||
// 子需求允许大需求关闭的状态集合
|
||||
private static final List<String> CHILD_ALLOW_CLOSE_STATUSES = List.of(STATUS_CLOSED, STATUS_CANCELLED, STATUS_REJECTED, STATUS_ACCEPTED);
|
||||
// 允许删除的状态集合(实施中之前的状态)
|
||||
private static final List<String> ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CONFIRM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH);
|
||||
|
||||
// 权限常量
|
||||
private static final String PRODUCT_CREATE_PERMISSION = "project:product:create";
|
||||
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_SPLIT_PERMISSION = "project:product:split";
|
||||
|
||||
// 审计动作常量
|
||||
private static final String ACTION_CREATE = "create";
|
||||
private static final String ACTION_UPDATE = "update";
|
||||
private static final String ACTION_DELETE = "delete";
|
||||
private static final String ACTION_SPLIT = "split";
|
||||
private static final String ACTION_CLOSE = "close";
|
||||
private static final String BIZ_TYPE_REQUIREMENT = "product_requirement";
|
||||
|
||||
@Resource
|
||||
private ProductRequirementMapper requirementMapper;
|
||||
@Resource
|
||||
private ProductRequirementModuleMapper moduleMapper;
|
||||
@Resource
|
||||
private ProductRequirementStatusLogMapper statusLogMapper;
|
||||
@Resource
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@Resource
|
||||
private ObjectStatusTransitionMapper statusTransitionMapper;
|
||||
@Resource
|
||||
private ObjectStatusModelMapper statusModelMapper;
|
||||
|
||||
// ========== 需求增删改查 ==========
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#createReqVO.productId",
|
||||
permission = PRODUCT_CREATE_PERMISSION)
|
||||
public Long createRequirement(ProductRequirementSaveReqVO createReqVO) {
|
||||
// 当未选择模块时,自动归属到该产品的"全部需求"模块
|
||||
Long moduleId = resolveModuleId(createReqVO.getModuleId(), createReqVO.getProductId());
|
||||
// 校验模块是否属于当前产品
|
||||
validateModuleBelongsToProduct(moduleId, createReqVO.getProductId());
|
||||
|
||||
ProductRequirementDO requirement = new ProductRequirementDO();
|
||||
requirement.setProductId(createReqVO.getProductId());
|
||||
requirement.setParentId(0L); // 新增需求默认是顶级需求
|
||||
requirement.setModuleId(moduleId);
|
||||
requirement.setReviewRequired(createReqVO.getReviewRequired());
|
||||
requirement.setTitle(createReqVO.getTitle().trim());
|
||||
requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
|
||||
requirement.setCategory(createReqVO.getCategory());
|
||||
requirement.setSourceType("manual"); // 手工新增默认来源类型
|
||||
requirement.setPriority(createReqVO.getPriority());
|
||||
// 根据是否需要评审确定初始状态
|
||||
String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
|
||||
? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
|
||||
requirement.setStatusCode(initialStatus);
|
||||
requirement.setProposerId(createReqVO.getProposerId());
|
||||
requirement.setCurrentHandlerUserId(createReqVO.getCurrentHandlerUserId());
|
||||
requirement.setImplementProjectId(createReqVO.getImplementProjectId());
|
||||
requirement.setCompletionDate(createReqVO.getCompletionDate());
|
||||
requirement.setSort(createReqVO.getSort() != null ? createReqVO.getSort() : 0);
|
||||
requirementMapper.insert(requirement);
|
||||
|
||||
// 写入业务审计日志
|
||||
writeBizAuditLog(requirement, ACTION_CREATE, null, initialStatus, null, null);
|
||||
return requirement.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#updateReqVO.productId",
|
||||
permission = PRODUCT_UPDATE_PERMISSION)
|
||||
public void updateRequirement(ProductRequirementUpdateReqVO updateReqVO) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
|
||||
// 校验终态不允许编辑
|
||||
validateRequirementEditable(requirement);
|
||||
// 当未选择模块时,自动归属到该产品的"全部需求"模块
|
||||
Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProductId());
|
||||
// 校验模块是否属于当前产品
|
||||
validateModuleBelongsToProduct(moduleId, updateReqVO.getProductId());
|
||||
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
requirement.setModuleId(moduleId);
|
||||
requirement.setReviewRequired(updateReqVO.getReviewRequired());
|
||||
requirement.setTitle(updateReqVO.getTitle().trim());
|
||||
requirement.setDescription(normalizeNullableText(updateReqVO.getDescription()));
|
||||
requirement.setCategory(updateReqVO.getCategory());
|
||||
requirement.setPriority(updateReqVO.getPriority());
|
||||
requirement.setProposerId(updateReqVO.getProposerId());
|
||||
requirement.setCurrentHandlerUserId(updateReqVO.getCurrentHandlerUserId());
|
||||
requirement.setImplementProjectId(updateReqVO.getImplementProjectId());
|
||||
requirement.setCompletionDate(updateReqVO.getCompletionDate());
|
||||
requirement.setSort(updateReqVO.getSort() != null ? updateReqVO.getSort() : 0);
|
||||
requirementMapper.updateById(requirement);
|
||||
|
||||
writeBizAuditLog(requirement, ACTION_UPDATE, fromStatus, requirement.getStatusCode(), null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public ProductRequirementRespVO getRequirement(Long id, Long productId) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(id);
|
||||
return buildRequirementRespVO(requirement);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#pageReqVO.productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO) {
|
||||
// 处理模块ID条件:支持递归查询子模块
|
||||
if (pageReqVO.getModuleId() != null) {
|
||||
if (isAllRequirementsModule(pageReqVO.getModuleId())) {
|
||||
// "全部需求"模块,忽略模块ID条件,查询该产品下所有需求
|
||||
pageReqVO.setModuleId(null);
|
||||
} else {
|
||||
// 非"全部需求"模块,获取该模块及其所有子模块的ID列表
|
||||
List<Long> moduleIds = getAllModuleIdsWithChildren(pageReqVO.getModuleId(), pageReqVO.getProductId());
|
||||
pageReqVO.setModuleIds(moduleIds);
|
||||
pageReqVO.setModuleId(null);
|
||||
}
|
||||
}
|
||||
PageResult<ProductRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO);
|
||||
List<ProductRequirementRespVO> list = pageResult.getList().stream()
|
||||
.map(this::buildRequirementRespVO)
|
||||
.collect(Collectors.toList());
|
||||
return new PageResult<>(list, pageResult.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#pageReqVO.productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public PageResult<ProductRequirementRespVO> getRequirementTree(ProductRequirementPageReqVO pageReqVO) {
|
||||
Long moduleId = pageReqVO.getModuleId();
|
||||
Long productId = pageReqVO.getProductId();
|
||||
|
||||
// 处理模块过滤条件:仅当选中具体模块时,递归加载子模块ID进行过滤
|
||||
if (moduleId != null) {
|
||||
pageReqVO.setModuleIds(getAllModuleIdsWithChildren(moduleId, productId));
|
||||
pageReqVO.setModuleId(null);
|
||||
}
|
||||
// 清空parentId,查询所有符合条件的需求(不限层级)
|
||||
pageReqVO.setParentId(null);
|
||||
|
||||
// 第一步:查询所有符合搜索条件的需求
|
||||
List<ProductRequirementDO> matchedRequirements = requirementMapper.selectList(pageReqVO);
|
||||
if (matchedRequirements.isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L);
|
||||
}
|
||||
|
||||
// 第二步:找出所有匹配需求的根节点ID,同时收集路径上的所有节点ID
|
||||
Set<Long> rootIds = new HashSet<>();
|
||||
Set<Long> pathNodeIds = new HashSet<>();
|
||||
Map<Long, ProductRequirementDO> requirementCache = new HashMap<>();
|
||||
for (ProductRequirementDO req : matchedRequirements) {
|
||||
requirementCache.put(req.getId(), req);
|
||||
pathNodeIds.add(req.getId());
|
||||
Long rootId = findRootRequirementIdAndCollectPath(req, requirementCache, pathNodeIds);
|
||||
rootIds.add(rootId);
|
||||
}
|
||||
|
||||
// 第三步:查询根需求详情并按创建时间倒排
|
||||
List<ProductRequirementDO> rootRequirements = requirementMapper.selectBatchIds(rootIds);
|
||||
rootRequirements.sort((a, b) -> b.getCreateTime().compareTo(a.getCreateTime()));
|
||||
|
||||
// 第四步:对根节点列表进行内存分页
|
||||
int pageNo = pageReqVO.getPageNo() != null ? pageReqVO.getPageNo() : 1;
|
||||
int pageSize = pageReqVO.getPageSize() != null ? pageReqVO.getPageSize() : 10;
|
||||
int total = rootRequirements.size();
|
||||
int fromIndex = (pageNo - 1) * pageSize;
|
||||
int toIndex = Math.min(fromIndex + pageSize, total);
|
||||
|
||||
if (fromIndex >= total) {
|
||||
return new PageResult<>(Collections.emptyList(), (long) total);
|
||||
}
|
||||
|
||||
List<ProductRequirementDO> pagedRootRequirements = rootRequirements.subList(fromIndex, toIndex);
|
||||
|
||||
// 第五步:构建树形结构(只包含路径上的节点)
|
||||
List<ProductRequirementRespVO> list = pagedRootRequirements.stream()
|
||||
.map(req -> buildRequirementRespVOWithPathChildren(req, pathNodeIds))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new PageResult<>(list, (long) total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上追溯需求的根节点ID,同时收集路径上的所有节点ID
|
||||
* @param requirement 起始需求
|
||||
* @param cache 需求缓存(避免重复查询)
|
||||
* @param pathNodeIds 路径节点ID集合(输出参数)
|
||||
* @return 根节点ID(parentId = 0L 的需求ID)
|
||||
*/
|
||||
private Long findRootRequirementIdAndCollectPath(ProductRequirementDO requirement,
|
||||
Map<Long, ProductRequirementDO> cache,
|
||||
Set<Long> pathNodeIds) {
|
||||
if (requirement.getParentId() == null || requirement.getParentId() == 0L) {
|
||||
return requirement.getId();
|
||||
}
|
||||
// 从缓存中查找父需求,如果没有则查询数据库
|
||||
ProductRequirementDO parent = cache.get(requirement.getParentId());
|
||||
if (parent == null) {
|
||||
parent = requirementMapper.selectById(requirement.getParentId());
|
||||
if (parent != null) {
|
||||
cache.put(parent.getId(), parent);
|
||||
}
|
||||
}
|
||||
if (parent == null) {
|
||||
// 父需求不存在(数据异常),返回当前需求作为根
|
||||
return requirement.getId();
|
||||
}
|
||||
// 收集路径上的节点ID
|
||||
pathNodeIds.add(parent.getId());
|
||||
return findRootRequirementIdAndCollectPath(parent, cache, pathNodeIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建需求响应VO(只包含路径上的子需求)
|
||||
* @param requirement 需求
|
||||
* @param pathNodeIds 路径节点ID集合
|
||||
* @return 需求响应VO
|
||||
*/
|
||||
private ProductRequirementRespVO buildRequirementRespVOWithPathChildren(ProductRequirementDO requirement,
|
||||
Set<Long> pathNodeIds) {
|
||||
ProductRequirementRespVO respVO = buildRequirementRespVO(requirement);
|
||||
// 查询子需求
|
||||
List<ProductRequirementDO> allChildren = requirementMapper.selectListByParentId(requirement.getId());
|
||||
// 只保留路径上的子需求
|
||||
List<ProductRequirementDO> pathChildren = allChildren.stream()
|
||||
.filter(child -> pathNodeIds.contains(child.getId()))
|
||||
.toList();
|
||||
if (!pathChildren.isEmpty()) {
|
||||
respVO.setChildren(pathChildren.stream()
|
||||
.map(child -> buildRequirementRespVOWithPathChildren(child, pathNodeIds))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定模块是否为"全部需求"模块(parentId = 0L 的根模块)
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isAllRequirementsModule(Long moduleId) {
|
||||
if (moduleId == null) {
|
||||
return false;
|
||||
}
|
||||
ProductRequirementModuleDO module = moduleMapper.selectById(moduleId);
|
||||
return module != null && module.getParentId() != null && module.getParentId() == 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取模块及其所有子模块的ID列表
|
||||
* @param moduleId 起始模块ID
|
||||
* @param productId 产品ID
|
||||
* @return 包含自身及所有子模块的ID列表
|
||||
*/
|
||||
@VisibleForTesting
|
||||
List<Long> getAllModuleIdsWithChildren(Long moduleId, Long productId) {
|
||||
List<Long> moduleIds = new ArrayList<>();
|
||||
moduleIds.add(moduleId);
|
||||
// 查询该产品下所有模块
|
||||
List<ProductRequirementModuleDO> allModules = moduleMapper.selectListByProductId(productId);
|
||||
// 递归查找子模块
|
||||
collectChildModuleIds(moduleId, allModules, moduleIds);
|
||||
return moduleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集子模块ID
|
||||
*/
|
||||
private void collectChildModuleIds(Long parentId, List<ProductRequirementModuleDO> allModules, List<Long> result) {
|
||||
for (ProductRequirementModuleDO module : allModules) {
|
||||
if (Objects.equals(module.getParentId(), parentId)) {
|
||||
result.add(module.getId());
|
||||
collectChildModuleIds(module.getId(), allModules, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取需求及其所有子需求(包含子子需求)
|
||||
* @param requirementId 起始需求ID
|
||||
* @return 包含自身及所有后代需求的列表
|
||||
*/
|
||||
@VisibleForTesting
|
||||
List<ProductRequirementDO> getAllRequirementsWithChildren(Long requirementId) {
|
||||
List<ProductRequirementDO> allRequirements = new ArrayList<>();
|
||||
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
||||
allRequirements.add(requirement);
|
||||
collectChildRequirements(requirementId, allRequirements);
|
||||
return allRequirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集子需求
|
||||
*/
|
||||
private void collectChildRequirements(Long parentId, List<ProductRequirementDO> result) {
|
||||
List<ProductRequirementDO> children = requirementMapper.selectListByParentId(parentId);
|
||||
for (ProductRequirementDO child : children) {
|
||||
result.add(child);
|
||||
collectChildRequirements(child.getId(), result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_STATUS_PERMISSION)
|
||||
public void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
||||
String actionCode = reqVO.getActionCode().trim();
|
||||
Long implementProjectId = reqVO.getImplementProjectId();
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
|
||||
// 校验状态流转是否合法
|
||||
ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
|
||||
String reason = normalizeNullableText(reqVO.getReason());
|
||||
// 校验是否需要填写原因
|
||||
validateTransitionReason(transition, reason);
|
||||
//下一状态
|
||||
String toStatus = transition.getToStatusCode();
|
||||
|
||||
// accept和close动作时,校验所有子需求(包括子子需求)是否处于允许状态
|
||||
if ("accept".equals(actionCode) || "close".equals(actionCode)) {
|
||||
validateAllChildrenAllowCloseOrAccept(reqVO.getId());
|
||||
}
|
||||
// close动作时,递归关闭所有已验收的子需求(包括子子需求)
|
||||
if ("close".equals(actionCode)) {
|
||||
closeAllAcceptedChildren(reqVO.getId(), reason);
|
||||
}
|
||||
// 带并发控制的状态更新(支持同时更新实现项目ID)
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatusWithProject(requirement.getId(), fromStatus, toStatus, reason, implementProjectId);
|
||||
if (updateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
requirement.setStatusCode(toStatus);
|
||||
requirement.setLastStatusReason(reason);
|
||||
|
||||
// 写入状态变更日志
|
||||
writeRequirementStatusLog(requirement, actionCode, fromStatus, toStatus, reason);
|
||||
// 写入业务审计日志
|
||||
writeBizAuditLog(requirement, actionCode, fromStatus, toStatus, null, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验需求的所有子需求(包括子子需求)是否处于允许关闭或验收的状态
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
|
||||
List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
|
||||
// 排除自身,只校验子需求
|
||||
for (ProductRequirementDO req : allChildren) {
|
||||
if (!Objects.equals(req.getId(), requirementId)) {
|
||||
if (!CHILD_ALLOW_CLOSE_STATUSES.contains(req.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_DELETE_PERMISSION)
|
||||
public void deleteRequirement(Long id, Long productId) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(id);
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
|
||||
// 校验是否存在子需求
|
||||
List<ProductRequirementDO> children = requirementMapper.selectListByParentId(id);
|
||||
if (!children.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_HAS_CHILDREN);
|
||||
}
|
||||
|
||||
// 校验状态是否允许删除(只有待确认、待评审、待分流状态才能删除)
|
||||
if (!ALLOW_DELETE_STATUSES.contains(fromStatus)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_DELETE);
|
||||
}
|
||||
|
||||
// 带并发控制的删除(以当前状态作为条件)
|
||||
int deleteCount = requirementMapper.deleteByIdAndStatus(id, fromStatus);
|
||||
if (deleteCount != 1) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
|
||||
writeBizAuditLog(requirement, ACTION_DELETE, fromStatus, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_SPLIT_PERMISSION)
|
||||
public Long splitRequirement(ProductRequirementSplitReqVO reqVO) {
|
||||
// 校验父需求是否存在
|
||||
ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId());
|
||||
// 校验父需求状态是否允许拆分(只能是待分流或实施中)
|
||||
validateParentAllowSplit(parentRequirement);
|
||||
|
||||
// 创建子需求
|
||||
ProductRequirementDO childRequirement = new ProductRequirementDO();
|
||||
childRequirement.setParentId(reqVO.getParentId());
|
||||
childRequirement.setProductId(parentRequirement.getProductId()); // 子需求继承父需求的产品
|
||||
childRequirement.setModuleId(parentRequirement.getModuleId()); // 子需求继承父需求的模块
|
||||
childRequirement.setReviewRequired(reqVO.getReviewRequired()); // 子需求默认不需要评审
|
||||
childRequirement.setTitle(reqVO.getTitle().trim());
|
||||
childRequirement.setDescription(normalizeNullableText(reqVO.getDescription()));
|
||||
childRequirement.setCategory(reqVO.getCategory());
|
||||
childRequirement.setSourceType(parentRequirement.getSourceType()); // 继承父需求来源类型
|
||||
childRequirement.setPriority(reqVO.getPriority());
|
||||
// 子需求初始状态为待分流
|
||||
// 根据是否需要评审确定初始状态
|
||||
String initialStatus = Objects.equals(reqVO.getReviewRequired(), 1)
|
||||
? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
|
||||
childRequirement.setStatusCode(initialStatus);
|
||||
childRequirement.setProposerId(parentRequirement.getProposerId()); // 继承父需求提出人
|
||||
childRequirement.setCurrentHandlerUserId(reqVO.getCurrentHandlerUserId());
|
||||
childRequirement.setImplementProjectId(reqVO.getImplementProjectId());
|
||||
childRequirement.setCompletionDate(reqVO.getCompletionDate());
|
||||
childRequirement.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
|
||||
requirementMapper.insert(childRequirement);
|
||||
|
||||
// 父需求状态从待分流变为实施中
|
||||
if (STATUS_PENDING_DISPATCH.equals(parentRequirement.getStatusCode())) {
|
||||
String fromStatus = parentRequirement.getStatusCode();
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||
parentRequirement.getId(), fromStatus, STATUS_IMPLEMENTING, null);
|
||||
if (updateCount == 1) {
|
||||
parentRequirement.setStatusCode(STATUS_IMPLEMENTING);
|
||||
writeRequirementStatusLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null);
|
||||
writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// 写入子需求的业务审计日志
|
||||
writeBizAuditLog(childRequirement, ACTION_CREATE, null, initialStatus, null, null);
|
||||
return childRequirement.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_STATUS_PERMISSION)
|
||||
public void closeRequirement(ProductRequirementCloseReqVO reqVO) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
|
||||
String fromStatus = requirement.getStatusCode();
|
||||
String reason = reqVO.getReason().trim();
|
||||
|
||||
// 校验当前状态是否为已验收(只有已验收才能关闭)
|
||||
if (!STATUS_ACCEPTED.equals(fromStatus)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_CLOSE);
|
||||
}
|
||||
|
||||
// 校验所有子需求(包括子子需求)是否允许关闭
|
||||
validateAllChildrenAllowCloseOrAccept(reqVO.getId());
|
||||
|
||||
// 递归关闭所有已验收的子需求(包括子子需求)
|
||||
closeAllAcceptedChildren(reqVO.getId(), reason);
|
||||
|
||||
// 关闭当前需求
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||
requirement.getId(), fromStatus, STATUS_CLOSED, reason);
|
||||
if (updateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
requirement.setStatusCode(STATUS_CLOSED);
|
||||
requirement.setLastStatusReason(reason);
|
||||
|
||||
writeRequirementStatusLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, reason);
|
||||
writeBizAuditLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, null, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归关闭所有已验收的子需求(包括子子需求)
|
||||
*/
|
||||
private void closeAllAcceptedChildren(Long parentId, String reason) {
|
||||
List<ProductRequirementDO> children = requirementMapper.selectListByParentId(parentId);
|
||||
for (ProductRequirementDO child : children) {
|
||||
// 递归处理子需求的子需求
|
||||
closeAllAcceptedChildren(child.getId(), reason);
|
||||
// 如果子需求已验收,则关闭
|
||||
if (STATUS_ACCEPTED.equals(child.getStatusCode())) {
|
||||
int updateCount = requirementMapper.updateStatusByIdAndStatus(
|
||||
child.getId(), STATUS_ACCEPTED, STATUS_CLOSED, reason);
|
||||
if (updateCount == 1) {
|
||||
writeRequirementStatusLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, reason);
|
||||
writeBizAuditLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, null, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
||||
String currentStatus = requirement.getStatusCode();
|
||||
|
||||
// 查询当前状态允许的所有流转
|
||||
List<ObjectStatusTransitionDO> transitions = statusTransitionMapper
|
||||
.selectListByObjectTypeAndFromStatus(REQUIREMENT_OBJECT_TYPE, currentStatus);
|
||||
|
||||
return transitions.stream().map(transition -> {
|
||||
ProductRequirementStatusTransitionRespVO vo = new ProductRequirementStatusTransitionRespVO();
|
||||
vo.setActionCode(transition.getActionCode());
|
||||
vo.setActionName(transition.getActionName());
|
||||
vo.setToStatusCode(transition.getToStatusCode());
|
||||
// 查询目标状态名称
|
||||
ObjectStatusModelDO statusModel = statusModelMapper
|
||||
.selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, transition.getToStatusCode());
|
||||
vo.setToStatusName(statusModel != null ? statusModel.getStatusName() : transition.getToStatusCode());
|
||||
vo.setNeedReason(transition.getNeedReason());
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public ProductRequirementLifecycleRespVO getRequirementLifecycle(Long requirementId, Long productId) {
|
||||
ProductRequirementDO requirement = validateRequirementExists(requirementId);
|
||||
String currentStatus = requirement.getStatusCode();
|
||||
|
||||
// 查询当前状态定义
|
||||
ObjectStatusModelDO statusModel = statusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(REQUIREMENT_OBJECT_TYPE, currentStatus);
|
||||
if (statusModel == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
|
||||
ProductRequirementLifecycleRespVO lifecycle = new ProductRequirementLifecycleRespVO();
|
||||
lifecycle.setStatusCode(statusModel.getStatusCode());
|
||||
lifecycle.setStatusName(statusModel.getStatusName());
|
||||
lifecycle.setTerminal(statusModel.getTerminalFlag());
|
||||
lifecycle.setAllowEdit(statusModel.getAllowEdit());
|
||||
lifecycle.setLastStatusReason(requirement.getLastStatusReason());
|
||||
lifecycle.setAvailableActions(getAllowedTransitions(requirementId, productId));
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
// ========== 模块管理 ==========
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_CREATE_PERMISSION)
|
||||
public Long createRequirementModule(ProductRequirementModuleReqVO reqVO) {
|
||||
// 校验模块名称在同一产品下是否唯一
|
||||
validateModuleNameUnique(reqVO.getProductId(), null, reqVO.getModuleName());
|
||||
|
||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
||||
module.setParentId(reqVO.getParentId() != null ? reqVO.getParentId() : 0L);
|
||||
module.setProductId(reqVO.getProductId());
|
||||
module.setModuleName(reqVO.getModuleName().trim());
|
||||
module.setRemark(normalizeNullableText(reqVO.getRemark()));
|
||||
module.setIcon(normalizeNullableText(reqVO.getIcon()));
|
||||
module.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
|
||||
moduleMapper.insert(module);
|
||||
return module.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
|
||||
permission = PRODUCT_UPDATE_PERMISSION)
|
||||
public void updateRequirementModule(ProductRequirementModuleReqVO reqVO) {
|
||||
if (reqVO.getId() == null) {
|
||||
throw invalidParamException("模块编号不能为空");
|
||||
}
|
||||
ProductRequirementModuleDO module = validateModuleExists(reqVO.getId());
|
||||
// 校验模块名称唯一性
|
||||
validateModuleNameUnique(reqVO.getProductId(), reqVO.getId(), reqVO.getModuleName());
|
||||
|
||||
module.setModuleName(reqVO.getModuleName().trim());
|
||||
module.setRemark(normalizeNullableText(reqVO.getRemark()));
|
||||
module.setIcon(normalizeNullableText(reqVO.getIcon()));
|
||||
module.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
|
||||
moduleMapper.updateById(module);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_DELETE_PERMISSION)
|
||||
public void deleteRequirementModule(Long moduleId, Long productId) {
|
||||
validateModuleExists(moduleId);
|
||||
|
||||
// 校验是否存在子模块
|
||||
List<ProductRequirementModuleDO> childModules = moduleMapper.selectListByParentId(moduleId);
|
||||
if (!childModules.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_CHILDREN);
|
||||
}
|
||||
|
||||
// 校验模块下是否存在需求
|
||||
List<ProductRequirementDO> requirements = requirementMapper.selectListByModuleId(moduleId);
|
||||
if (!requirements.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_REQUIREMENTS);
|
||||
}
|
||||
|
||||
// 删除模块
|
||||
moduleMapper.deleteById(moduleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
|
||||
permission = PRODUCT_QUERY_PERMISSION)
|
||||
public List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId) {
|
||||
List<ProductRequirementModuleDO> modules = moduleMapper.selectListByProductId(productId);
|
||||
return buildModuleTree(modules, 0L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProductRequirementStatusDictRespVO> getRequirementStatusDict() {
|
||||
List<ObjectStatusModelDO> statusModels = statusModelMapper.selectListByObjectType(REQUIREMENT_OBJECT_TYPE);
|
||||
return statusModels.stream()
|
||||
.map(this::buildStatusDictRespVO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict() {
|
||||
List<ObjectStatusModelDO> statusModels = statusModelMapper.selectListByObjectType(REQUIREMENT_OBJECT_TYPE);
|
||||
return statusModels.stream()
|
||||
.filter(ObjectStatusModelDO::getTerminalFlag)
|
||||
.map(this::buildStatusDictRespVO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建状态字典响应VO
|
||||
*/
|
||||
private ProductRequirementStatusDictRespVO buildStatusDictRespVO(ObjectStatusModelDO statusModel) {
|
||||
ProductRequirementStatusDictRespVO respVO = new ProductRequirementStatusDictRespVO();
|
||||
respVO.setStatusCode(statusModel.getStatusCode());
|
||||
respVO.setStatusName(statusModel.getStatusName());
|
||||
respVO.setSort(statusModel.getSort());
|
||||
respVO.setInitialFlag(statusModel.getInitialFlag());
|
||||
respVO.setTerminalFlag(statusModel.getTerminalFlag());
|
||||
return respVO;
|
||||
}
|
||||
|
||||
// ========== 私有辅助方法 ==========
|
||||
|
||||
/**
|
||||
* 构建需求响应VO(不含子需求)
|
||||
*/
|
||||
private ProductRequirementRespVO buildRequirementRespVO(ProductRequirementDO requirement) {
|
||||
ProductRequirementRespVO respVO = BeanUtils.toBean(requirement, ProductRequirementRespVO.class);
|
||||
// 查询状态名称
|
||||
ObjectStatusModelDO statusModel = statusModelMapper
|
||||
.selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode());
|
||||
if (statusModel != null) {
|
||||
respVO.setStatusName(statusModel.getStatusName());
|
||||
respVO.setTerminal(statusModel.getTerminalFlag());
|
||||
}
|
||||
// 设置是否终态
|
||||
if (respVO.getTerminal() == null) {
|
||||
respVO.setTerminal(TERMINAL_STATUSES.contains(requirement.getStatusCode()));
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建模块树
|
||||
*/
|
||||
private List<ProductRequirementModuleRespVO> buildModuleTree(List<ProductRequirementModuleDO> modules, Long parentId) {
|
||||
return modules.stream()
|
||||
.filter(m -> Objects.equals(m.getParentId(), parentId))
|
||||
.map(m -> {
|
||||
ProductRequirementModuleRespVO vo = BeanUtils.toBean(m, ProductRequirementModuleRespVO.class);
|
||||
vo.setChildren(buildModuleTree(modules, m.getId()));
|
||||
return vo;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验需求是否存在
|
||||
*/
|
||||
@VisibleForTesting
|
||||
ProductRequirementDO validateRequirementExists(Long id) {
|
||||
if (id == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS);
|
||||
}
|
||||
ProductRequirementDO requirement = requirementMapper.selectById(id);
|
||||
if (requirement == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS);
|
||||
}
|
||||
return requirement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验需求是否允许编辑(终态不允许编辑)
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateRequirementEditable(ProductRequirementDO requirement) {
|
||||
if (TERMINAL_STATUSES.contains(requirement.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验父需求是否允许拆分
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateParentAllowSplit(ProductRequirementDO parentRequirement) {
|
||||
String status = parentRequirement.getStatusCode();
|
||||
if (!STATUS_PENDING_DISPATCH.equals(status) && !STATUS_IMPLEMENTING.equals(status)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验状态流转是否合法
|
||||
*/
|
||||
@VisibleForTesting
|
||||
ObjectStatusTransitionDO validateRequirementTransition(String fromStatusCode, String actionCode) {
|
||||
ObjectStatusTransitionDO transition = statusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
}
|
||||
return transition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验状态流转是否需要填写原因
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验模块是否存在
|
||||
*/
|
||||
@VisibleForTesting
|
||||
ProductRequirementModuleDO validateModuleExists(Long id) {
|
||||
if (id == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
|
||||
}
|
||||
ProductRequirementModuleDO module = moduleMapper.selectById(id);
|
||||
if (module == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验模块名称在同一产品下是否唯一
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateModuleNameUnique(Long productId, Long moduleId, String moduleName) {
|
||||
if (!StringUtils.hasText(moduleName)) {
|
||||
return;
|
||||
}
|
||||
ProductRequirementModuleDO existModule = moduleMapper
|
||||
.selectByProductIdAndModuleName(productId, moduleName.trim());
|
||||
if (existModule == null) {
|
||||
return;
|
||||
}
|
||||
if (!existModule.getId().equals(moduleId)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NAME_DUPLICATE, moduleName.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析模块ID:当未选择模块时,自动归属到该产品的"全部需求"模块
|
||||
* "全部需求"模块的标志性特征是 parentId = 0L
|
||||
*/
|
||||
@VisibleForTesting
|
||||
Long resolveModuleId(Long moduleId, Long productId) {
|
||||
if (moduleId != null) {
|
||||
return moduleId;
|
||||
}
|
||||
// 查询该产品的"全部需求"模块(parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = moduleMapper
|
||||
.selectByProductIdAndParentId(productId, 0L);
|
||||
if (defaultModule == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
|
||||
}
|
||||
return defaultModule.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验模块是否属于当前产品
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void validateModuleBelongsToProduct(Long moduleId, Long productId) {
|
||||
if (moduleId == null) {
|
||||
return; // 全部需求模块不需要校验
|
||||
}
|
||||
ProductRequirementModuleDO module = moduleMapper.selectById(moduleId);
|
||||
if (module == null) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
|
||||
}
|
||||
if (!Objects.equals(module.getProductId(), productId)) {
|
||||
throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_BELONG_TO_PRODUCT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入需求状态变更日志
|
||||
*/
|
||||
private void writeRequirementStatusLog(ProductRequirementDO requirement, String actionType,
|
||||
String fromStatus, String toStatus, String reason) {
|
||||
ProductRequirementStatusLogDO statusLog = new ProductRequirementStatusLogDO();
|
||||
statusLog.setRequirementId(requirement.getId());
|
||||
statusLog.setActionType(actionType);
|
||||
statusLog.setFromStatus(fromStatus);
|
||||
statusLog.setToStatus(toStatus);
|
||||
statusLog.setReason(defaultText(reason));
|
||||
statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
statusLog.setRequirementTitleSnapshot(requirement.getTitle());
|
||||
statusLogMapper.insert(statusLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入业务审计日志
|
||||
*/
|
||||
private void writeBizAuditLog(ProductRequirementDO requirement, String actionType, String fromStatus,
|
||||
String toStatus, String fieldChanges, String reason) {
|
||||
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||
auditLog.setBizType(BIZ_TYPE_REQUIREMENT);
|
||||
auditLog.setBizId(requirement.getId());
|
||||
auditLog.setActionType(actionType);
|
||||
auditLog.setFromStatus(fromStatus);
|
||||
auditLog.setToStatus(toStatus);
|
||||
auditLog.setFieldChanges(fieldChanges);
|
||||
auditLog.setReason(reason);
|
||||
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理可能为空的文本,去除首尾空格后若为空则返回null
|
||||
*/
|
||||
private String normalizeNullableText(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理可能为空的文本,若为空则返回空字符串
|
||||
*/
|
||||
private String defaultText(String value) {
|
||||
return StringUtils.hasText(value) ? value : "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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.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.setting.ProductSettingBaseInfoUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
|
||||
/**
|
||||
* 产品 Service 接口
|
||||
*/
|
||||
public interface ProductService {
|
||||
|
||||
/**
|
||||
* 创建产品
|
||||
*
|
||||
* @param createReqVO 创建请求
|
||||
* @return 产品编号
|
||||
*/
|
||||
Long createProduct(ProductSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新产品
|
||||
*
|
||||
* @param updateReqVO 更新请求
|
||||
*/
|
||||
void updateProduct(ProductSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 更新产品设置页基础信息
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @param reqVO 更新请求
|
||||
*/
|
||||
void updateProductBaseInfo(Long productId, ProductSettingBaseInfoUpdateReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 获取产品详情
|
||||
*
|
||||
* @param id 产品编号
|
||||
* @return 产品信息
|
||||
*/
|
||||
ProductDO getProduct(Long id);
|
||||
|
||||
/**
|
||||
* 获取产品上下文
|
||||
*
|
||||
* @param id 产品编号
|
||||
* @return 产品上下文
|
||||
*/
|
||||
ProductContextRespVO getProductContext(Long id);
|
||||
|
||||
/**
|
||||
* 获取产品分页
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 变更产品状态
|
||||
*
|
||||
* @param reqVO 状态动作请求
|
||||
*/
|
||||
void changeProductStatus(ProductStatusActionReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除产品
|
||||
*
|
||||
* @param reqVO 删除请求
|
||||
*/
|
||||
void deleteProduct(ProductDeleteReqVO reqVO);
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user