feat(other): 产品基础功能提交

This commit is contained in:
2026-04-18 14:19:45 +08:00
parent 0c91f5deaa
commit 38c69c748c
75 changed files with 5139 additions and 1047 deletions

30
rdms-project/pom.xml Normal file
View 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>

View 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>

View File

@@ -0,0 +1,4 @@
/**
* Project API 包,定义暴露给其它模块的 API
*/
package com.njcn.rdms.module.project.api;

View File

@@ -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() {
}
}

View File

@@ -0,0 +1,23 @@
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, "产品暂停后仅允许变更产品经理、描述和备注");
}

View File

@@ -0,0 +1,13 @@
package com.njcn.rdms.module.project.enums;
/**
* 项目交付域字典类型常量
*/
public interface ProjectDictTypeConstants {
/**
* 产品方向
*/
String PRODUCT_DIRECTION = "rdms_product_direction";
}

View 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>

View File

@@ -0,0 +1,49 @@
# 02-产品管理 SQL已确认口径
## 0. 文档说明
本文档用于记录产品管理 SQL 已确认的实现口径。
本文档只保留已确认结果,不保留待确认项、方案对比或历史演变说明。
## 1. 共享表承接边界
- `rdms_user_object_role`
- `rdms_object_status_model`
- `rdms_object_status_transition`
- `rdms_biz_audit_log`
以上共享表继续由 `rdms-project/rdms-project-boot/src/main/resources/sql/product/01_product_schema.sql` 承接。
## 2. 产品需求状态字段口径
- `rdms_product_requirement` 统一改为 `status_code` 口径。
- 产品需求状态与产品状态保持一致,统一使用状态编码模型。
## 3. 来源承接与需求拆分口径
- 产品需求既可能来自工单流转,也可能来自产品内手工新增。
- 产品需求不论来源,都允许继续拆分为 N 个子需求。
- 同一产品下,同一来源工单只生成 1 条源头需求记录。
- 源头需求记录可以拆分为 N 条子需求。
- 手工新增需求也可以拆分为 N 条子需求。
- 子需求不参与“来源唯一”约束。
- 来源追溯和拆分关系分开建模。
## 4. 需求终态原因承接口径
- 需求终态原因由主表承接当前结果态,同时审计日志保留完整留痕。
- 主表统一承接终态结果字段,覆盖 `reject``cancel``close` 等终态动作。
字段口径:
- `terminal_action_code`
- `terminal_reason`
- `terminal_time`
## 5. 当前确认结果
- 第 1 项:共享表继续由 `01_product_schema.sql` 承接
- 第 2 项:`rdms_product_requirement` 统一改为 `status_code`
- 第 3 项:来源承接与需求拆分分开建模
- 第 4 项:主表补终态结果字段,审计日志继续完整留痕

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,478 @@
# 03-产品管理范围内产品与产品需求链路与工作流方案
## 0. 文档目的
本文档用于回答以下问题:
- 在当前产品管理范围内,`product -> product_requirement` 是否适合做链路视图
- 后续工单进入产品侧时,应如何接入当前产品管理链路
- 是否适合引入 Flowable
- Flowable 在当前产品管理方案中应承接哪些节点,不应承接哪些节点
- 当前产品管理链路应如何建模
- 当前产品需求设计中的“终态原因”口径适用范围是什么
说明:
- 本文档中的“产品作为主上下文”仅限当前产品管理能力建设范围。
- 该口径用于当前产品管理方案收敛,不作为 RDMS 全局统一建模原则。
- 本文档只给方案不直接修改业务代码、SQL 或正式设计文档。
## 1. 当前现状判断
### 1.1 当前代码与文档现状
- 当前仓库未看到 Flowable 依赖、BPMN 定义或流程引擎接入实现。
- `rdms-project` 已开始承接通用对象状态模型,当前已有 `rdms_object_status_model``rdms_object_status_transition` 的 DO 和 Mapper。
- 产品管理当前已经明确:产品与产品需求承接轻量状态流转,但当前版本不展开正式工作流引擎。
- 当前产品需求设计已具备来源追溯、状态流转、认领 / 拒绝、拆分等业务语义。
- 当前要开发的是产品管理,不是项目管理;`project_requirement / execution / task` 不属于本期范围。
### 1.2 当前链路特征
在本期产品管理范围内,当前业务链路主要具备以下特征:
- 产品是当前方案的主上下文
- 产品侧可以手工新增产品需求
- 后续工单可以进入产品侧形成来源需求
- 一个产品需求可以拆分成 N 个子需求
- 某些节点需要人认领、人评审、人审批
- 某些节点只是普通业务状态推进,不适合上审批流
这意味着在本期产品管理范围内,该链路本质上是“产品上下文下的产品需求关系网络”,不是“一条从头跑到尾的单一审批流程”。
## 2. 核心结论
### 2.1 可行性判断
结论:可行,但本期不建议把产品管理范围内的所有动作都堆进单一 Flowable 流程。
推荐方向:
- 在本期产品管理方案内,产品作为当前链路的主上下文和主入口
- 当前链路聚合围绕 `product``product_requirement` 组织
- 需求链(`chain`)表示“本期产品管理范围内,产品下的一条源头需求链”
- 工单在后续接入时,只作为产品需求的来源关系之一,不作为链路根
- 业务主链统一由状态机控制
- Flowable 等以后开发评审流程时再接入,只承接评审、审批类协同节点
- 各业务对象继续维护自己的 `status_code`
- 流程状态不是业务真相源,业务表状态才是业务真相源
- 本期先按表和后端接口完成基础建模和后端闭环,不做前端页面和流程引擎接入
### 2.2 不建议单一长流程的原因
- 产品需求存在手工新增、父子拆分等多种情况;后续再叠加工单来源时,也难以用单流程表达。
- 认领、拒绝、拆分、关闭等动作很多是高频业务动作,不适合全部流程化。
- 流程状态和业务状态容易双写不一致。
- 当前真正需要先打通的是产品管理范围内的后端聚合能力,而不是先上流程图。
### 2.3 终态原因口径适用范围
“终态原因承接口径”不是只针对工单来源需求,而是针对产品需求对象本身。
也就是说,无论产品需求来自:
- 工单流转
- 手工新增
只要走到 `reject``cancel``close` 这类终态动作,都需要明确终态原因承接方式。
从后端聚合查询角度看,主表保留当前结果态原因字段,审计日志继续保留完整留痕。
## 3. 建议总体架构
当前建议拆成 4 层,再预留 1 个后续扩展层。
### 3.1 业务对象层
业务对象继续独立建模,独立维护生命周期和 `status_code`
本期纳入当前链路建模的对象:
- 产品 `product`
- 产品需求 `product_requirement`
- `work_order` 作为后续预留来源对象
要求:
- 在本期产品管理范围内,产品是当前链路的主上下文
- 产品需求是当前链路的核心业务对象
- 每个对象的状态变化由业务服务负责落库
- 后续工单进入产品侧后,只承接来源关系,不取代产品主上下文
- 业务主链先由状态机控制,不由流程引擎接管
### 3.2 关系模型层
关系模型用于描述对象之间的连接关系,而不是靠单个来源字段硬扛全部追溯逻辑。
本期至少需要表达以下关系:
- 产品和源头需求之间的主上下文关系
- 产品需求父子拆分
建议引入统一关系表,例如:
`rdms_biz_relation`
建议关键字段:
- `id`
- `chain_id`
- `product_id`
- `from_biz_type`
- `from_biz_id`
- `to_biz_type`
- `to_biz_id`
- `relation_type`
- `sort`
- `remark`
建议 `relation_type` 取值至少包括:
- `product_root`:产品主上下文
- `split_child`:拆分子需求
后续预留关系类型:
- `source_work_order`:来源工单
说明:
- `product_root` 用于表达本期产品管理范围内,产品和源头需求之间的主上下文关系。
- 手工新增不是对象间来源关系,不必强行补一条虚拟来源边;可由 `rdms_requirement_chain.entry_source_type = manual` 承接。
### 3.3 事件模型层
事件模型用于描述“产品下的一条需求链上发生了什么”,服务于后端聚合查询和后续时间线视图。
建议引入统一事件表,例如:
`rdms_biz_event`
建议关键字段:
- `id`
- `chain_id`
- `product_id`
- `biz_type`
- `biz_id`
- `event_type`
- `action_code`
- `from_status_code`
- `to_status_code`
- `operator_user_id`
- `operator_name`
- `reason`
- `event_time`
- `payload_json`
事件类型可覆盖:
- `create`
- `claim`
- `reject`
- `split`
- `review_pass`
- `review_reject`
- `close`
- `cancel`
后续预留事件类型:
- `source_attach`
### 3.4 需求链聚合层
为了做当前产品管理范围内的链路聚合,建议引入统一聚合对象。
建议引入需求链主表,例如:
`rdms_requirement_chain`
建议关键字段:
- `id`
- `chain_code`
- `product_id`
- `root_requirement_id`
- `entry_source_type`
- `entry_biz_type`
- `entry_biz_id`
- `title`
- `current_status_code`
- `closed_flag`
用途:
- 作为“产品下的一条源头需求链”的聚合单元
- 聚合关系和事件
- 支撑后端聚合查询;后续前端如接入,再由产品详情页和产品需求详情页承接
说明:
- 一个产品下会有多条需求链
- 一条需求链只属于一个产品
- 一条需求链只围绕一个源头产品需求展开
- 后续工单来源需求创建需求链时,工单只写入 `entry_biz_type / entry_biz_id``source_work_order` 关系,不作为根对象
### 3.5 流程绑定层(后续扩展)
流程引擎只负责协同过程,因此等以后开发评审流程时,可再补业务对象和流程实例的绑定层。
建议引入流程绑定表,例如:
`rdms_workflow_binding`
建议关键字段:
- `id`
- `chain_id`
- `product_id`
- `biz_type`
- `biz_id`
- `process_definition_key`
- `process_instance_id`
- `workflow_status`
- `current_task_key`
- `current_task_name`
- `starter_user_id`
- `start_time`
- `end_time`
说明:
- 该层不是本期前置条件
- 本期不接 Flowable 时,不需要先落这张表
- 等以后开发评审流程时再接入,只用于评审、审批类协同节点
## 4. Flowable 适用边界
### 4.1 适合接 Flowable 的节点
以下节点等以后开发评审流程时,可考虑接入 Flowable
- 产品需求待评审
- 高风险终态动作审批
- 其他明确的审批类节点
这类节点的共同特征是:
- 需要明确待办人
- 需要审批意见
- 需要驳回、转交、加签、会签等协作能力
### 4.2 不建议接 Flowable 的节点
以下节点不建议直接建成流程审批节点:
- 后续工单接入产品侧后的认领 / 拒绝
- 普通状态编辑
- 列表筛选查询
- 日常状态推进
- 产品需求拆分
这类节点更适合保留在业务状态机或普通业务服务里,否则流程实例会过多、过碎、过重。
## 5. 当前链路视图如何实现
本期只要求后端先具备聚合查询能力,不要求直接交付页面。
### 5.1 拓扑数据
展示本期产品管理范围内,以产品为入口的对象关系链:
```text
产品
-> 产品需求(源头需求)
-> 子需求
后续工单 -(source_work_order)-> 产品需求(可选来源)
```
后端主要聚合:
- `rdms_requirement_chain`
- `rdms_biz_relation`
- `product`
- `product_requirement`
### 5.2 时间线数据
展示产品下某条需求链上的关键事件:
- 产品需求创建
- 产品需求认领
- 产品需求评审
- 拆分子需求
- 关闭 / 拒绝 / 取消
后端主要聚合:
- `rdms_biz_event`
- `product_requirement` 当前状态摘要
说明:
- 时间线围绕单条需求链展开,不是把整个产品下所有动作混成一条总流水。
- 本期先交付后端聚合查询接口,不要求同时交付拓扑页和时间线页,也不进入前端联调。
## 6. 对现有产品管理设计的建议
### 6.1 本期产品管理范围内的主上下文口径
既然本期要开发的是产品管理,建议当前方案中的聚合查询和后端入口都优先挂在产品上下文下。
要求:
- `product_id` 是链路聚合模型的必填归属字段
- 后续前端如接入,产品详情页和产品需求详情页是主入口
- 后续工单详情页如需展示链路,只适合作为来源跳转入口,不适合作为主视图入口
### 6.2 状态口径
既然你已经确认本期只做产品和产品需求,建议当前阶段只要求这两个对象继续沿用统一的 `status_code` 口径。
说明:
- `product`
- `product_requirement`
项目管理阶段再单独设计 `project_requirement / execution / task` 的状态模型和流转规则,不并入本期范围。
### 6.3 来源与拆分口径
你已经确认:
- 来源承接和需求拆分分开建模
- 后续工单来源需求可拆分
- 手工新增需求也可拆分
当前产品需求口径应将来源追溯和拆分关系分开承接:
- `source_biz_*` 字段只承接来源追溯
- `parent_requirement_id``root_requirement_id` 承接拆分链路
- 后续同一产品下,同一来源工单只生成 1 条源头需求记录
- 子需求不参与来源唯一约束
### 6.4 终态原因口径
产品需求主表统一承接当前结果态字段。
当前字段方向:
- `terminal_action_code`
- `terminal_reason`
- `terminal_time`
这样后端做本期产品管理范围内的聚合查询时,可以直接读取当前结果态原因,而不用每次回扫审计日志。
## 7. 建议实施顺序
建议按三步走,但只有第一步属于本期必做范围。
### 第一步:先完成本期产品管理的基础建模和后端闭环
目标:
- 明确在本期产品管理范围内,产品作为当前链路的主上下文
- 建立链路主表
- 建立链路关系表
- 建立链路事件表
- 统一产品和产品需求的状态口径
- 先把业务主链的状态机闭环跑通
- 先提供产品上下文下查询单条需求链关系拓扑和事件时间线的聚合接口
这一阶段先不接 Flowable只按表和后端接口开发不做前端拓扑页和时间线页也不扩到项目管理对象。
### 第二步:后续开发评审流程时接入有限流程节点
目标:
- 产品需求评审
- 高风险终态审批
- 其他明确需要审批协同的节点
这类节点等以后开发评审流程时可接入 Flowable但不是本期前置条件。
### 第三步:后续在项目管理阶段扩展项目域对象
目标:
- 在后续项目管理建设时,再补 `project_requirement / execution / task` 等对象
- 届时再单独设计它们和产品需求之间的关系、事件和流程边界
说明:
- 这一步不属于本期产品管理开发范围。
## 8. 风险点与控制建议
### 8.1 范围扩散到项目管理
风险:
- 在当前产品管理开发过程中,又把 `project_requirement / execution / task` 一起拉进来
控制建议:
- 所有设计文档都明确“本期只做产品和产品需求”
- 项目域对象放到后续项目管理建设时再单独设计
### 8.2 状态双写不一致
风险:
- 流程状态更新了,但业务表状态没更新
- 业务状态更新了,但流程实例没推进
控制建议:
- 本期主链统一由状态机控制
- 等以后开发评审流程时接入 Flowable也只作为协同触发器
- 每次关键动作统一写 `rdms_biz_event`
### 8.3 来源与拆分关系混淆
风险:
- 来源关系和拆分关系混在一个字段里,后续追溯会失真
控制建议:
- 来源关系和父子拆分关系分开建模
- 统一走关系表,不靠单字段隐式表达
### 8.4 前端聚合过早介入
风险:
- 后端模型和接口还没稳住,就先开始拼页面,后面会频繁返工
控制建议:
- 本期先完成基础建模和聚合查询接口
- 前端页面放到后续接口稳定后再接入
## 9. 当前建议结论
当前建议拍板如下:
1. 在本期产品管理方案内,产品作为当前链路的主上下文。
2. 本期范围只包含 `product``product_requirement``work_order` 只作为后续预留来源对象,不纳入第一批实现。
3. 一条需求链(`chain`)表示“产品下的一条源头需求链”,不是整个产品,也不是某个工单。
4. 本期先做基础建模和后端闭环,包括 `requirement_chain / relation / event` 模型、状态机主链、关键动作落库和聚合查询接口。
5. 本期主链统一由状态机控制Flowable 等以后开发评审流程时再接入,只用于评审、审批类节点。
6. 来源追溯和需求拆分必须分开建模。
7. 产品需求终态原因由主表承接当前结果态,并继续保留审计日志完整留痕。
8. 项目域对象放到后续项目管理建设时再单独设计,不并入本期产品管理范围。
9. 以上口径仅用于当前产品管理建设,不作为 RDMS 全局统一建模原则。
## 10. 下一步建议产物
如果继续推进,建议下一步补 3 份专项文档:
1. `产品管理范围内链路接口设计.md`
明确聚合查询接口、状态动作接口的返回结构。
2. `产品管理范围内链路SQL与表结构设计.md`
明确 `requirement_chain / relation / event` 的表结构和索引设计。
3. `产品管理范围内链路开发顺序与任务拆分.md`
明确 SQL、DO、Mapper、Service、Controller 的开发顺序。

View File

@@ -0,0 +1,221 @@
# 04-产品管理 编码前必看清单
## 0. 文档目的
本文档用于在开始产品管理相关编码前,明确必须先阅读的文档、必须锁死的业务口径,以及当前文档与可执行 SQL 的同步状态。
本文档只保留当前编码前必须关注的内容,不保留历史演变说明。
## 1. 文档分级
### 1.1 一级依据
以下文档为产品管理编码主依据,涉及业务口径、接口口径、状态口径时,必须优先对齐:
- `docs/temp/02-产品管理_业务设计.md`
- `docs/temp/02-产品管理_SQL已确认口径.md`
### 1.2 二级依据
以下文档用于补齐 SQL 结构说明:
- `docs/temp/02-产品管理_SQL口径说明.md`
### 1.3 SQL 阅读基线
以下文件是当前产品管理编码前唯一需要阅读的 SQL 文件,用于确认当前表结构、状态模型、状态流转和审计表设计:
- `rdms-project/rdms-project-boot/product/rdms_biz_audit_log.sql`
说明:
- 当前 SQL 阅读入口统一以该文件为准。
- 该文件当前包含 `rdms_biz_audit_log``rdms_object_status_model``rdms_object_status_transition``rdms_product``rdms_product_requirement``rdms_product_status_log``rdms_user_object_role`
- 编码前必须先识别该文件与已确认口径之间的差异,不能直接把该文件当作最终目标结构。
### 1.4 场景性依据
以下文档只在涉及工单来源、链路视图、Flowable 边界时必读:
- `docs/temp/03-工单到任务全链路与工作流方案.md`
## 2. 阅读顺序
开始编码前,按以下顺序阅读:
1. `02-产品管理_业务设计.md`
2. `02-产品管理_SQL已确认口径.md`
3. `02-产品管理_SQL口径说明.md`
4. `rdms-project/rdms-project-boot/product/rdms_biz_audit_log.sql`
5. `03-工单到任务全链路与工作流方案.md`仅在涉及工单链路、需求拆分、Flowable、全链路视图时阅读
## 3. 每份文档必须看的内容
### 3.1 `02-产品管理_业务设计.md`
必须重点看以下内容:
- 模块范围与非本期范围
- 页面与对象上下文承载方式
- 产品需求规则
- 对象关系与数据设计
- 产品状态与产品需求状态机
- 接口承接
- 权限与动作矩阵
- 测试关注点
阅读目标:
- 明确本次到底做哪些页面、动作、字段和权限。
- 明确哪些能力本次不做,避免编码时扩散。
- 明确关闭、认领、拒绝、分流等动作的业务边界。
### 3.2 `02-产品管理_SQL已确认口径.md`
必须逐条看完以下 4 项:
- 共享表承接边界
- 产品需求状态字段口径
- 来源承接与需求拆分口径
- 需求终态原因承接口径
阅读目标:
- 锁死产品需求 `status_code` 口径。
- 锁死来源追溯和需求拆分分开建模。
- 锁死终态结果字段由主表承接。
### 3.3 `02-产品管理_SQL口径说明.md`
必须重点看以下内容:
- 共享表与主数据口径
- 产品需求口径
- 状态与留痕口径
阅读目标:
- 明确当前确认后的 SQL 结构表达方式。
- 明确状态编码、来源追溯、拆分链路、终态结果字段应该如何落到表结构上。
### 3.4 `rdms-project/rdms-project-boot/product/rdms_biz_audit_log.sql`
必须重点看以下内容:
- `rdms_biz_audit_log`
- `rdms_object_status_model`
- `rdms_object_status_transition`
- `rdms_product`
- `rdms_product_requirement`
- `rdms_product_status_log`
- `rdms_user_object_role`
阅读目标:
- 明确当前统一 SQL 阅读入口下的表结构、索引、状态种子数据和审计字段现状。
- 明确哪些字段和状态定义仍未对齐已确认口径。
### 3.5 `03-工单到任务全链路与工作流方案.md`
仅在以下场景必须看:
- 涉及工单流转到产品需求
- 涉及来源追溯与需求拆分
- 涉及产品需求到项目需求的链路设计
- 涉及 Flowable 接入边界
- 涉及全链路视图
阅读目标:
- 明确 Flowable 只承接协同节点,不直接承接整条业务主链状态。
- 明确来源关系、拆分关系、事件关系和流程绑定关系要分开建模。
## 4. 编码前必须锁死的业务口径
### 4.1 模块边界
- 产品管理能力落在 `rdms-project`,不落到 `rdms-system``rdms-gateway`
- 跨模块共享能力通过 `*-api` 承接,不直接依赖其他 `*-boot` 实现。
### 4.2 产品需求状态口径
- `rdms_product_requirement` 主表统一使用 `status_code`
- 产品需求状态定义与流转统一通过 `rdms_object_status_model``rdms_object_status_transition` 承接。
- `object_type` 统一使用 `product_requirement`
### 4.3 来源追溯与需求拆分口径
- 产品需求来源至少包括 `manual``work_order`
- `source_type``source_biz_type``source_biz_id``source_biz_code` 只承接来源追溯。
- `parent_requirement_id``root_requirement_id` 只承接拆分链路。
- 同一产品下,同一来源工单只允许 1 条源头需求记录。
- 源头需求和手工新增需求都允许继续拆分子需求。
- 子需求不参与来源唯一约束。
### 4.4 终态结果口径
- `reject``cancel``close` 等终态动作统一回写主表。
- 主表统一保留以下字段:
- `terminal_action_code`
- `terminal_reason`
- `terminal_time`
- 审计日志继续保留完整过程留痕。
### 4.5 接口动作口径
- 产品状态动作统一走 `POST /products/{id}/change-status`
- 产品需求状态动作统一走 `POST /products/{id}/requirements/{requirementId}/change-status`
- 产品需求 `change-status` 统一支持 `claim``to_review``to_dispatch``dispatch``reject``cancel``close`
- 状态动作不得混入通用编辑接口。
### 4.6 权限与审计口径
- 对象上下文权限按产品对象角色控制。
- 产品团队、产品需求、状态动作、敏感操作都必须落审计。
- 产品状态日志由 `rdms_product_status_log` 承接。
- 产品经理变更、成员调整、需求认领、拒绝、分流、关闭、拆分等由 `rdms_biz_audit_log` 承接。
## 5. 当前明确不做的内容
以下内容当前不纳入本轮编码:
- 产品版本
- 产品路线图
- 正式评审流
- Flowable 引擎接入实现
- 产品基线
- 产品文档与附件
- 工单状态回写
- 目标版本字段
- 完整链路视图页面
## 6. 当前现状与目标口径差异
`rdms_biz_audit_log.sql` 当前仍存在以下差异:
- `rdms_product_requirement` 仍使用 `status`,尚未切到 `status_code`
- 主表仍保留 `closed_reason``closed_time`
- 主表尚未补齐 `terminal_action_code``terminal_reason``terminal_time`
- 主表尚未补齐 `parent_requirement_id``root_requirement_id`
- 产品需求状态模型和状态流转种子尚未按 `product_requirement` 完整落齐
这意味着:
- 不能直接以当前 `rdms_biz_audit_log.sql` 作为产品需求最终结构开始编码。
- 如果进入正式开发,优先动作之一是先同步当前统一 SQL 文件,再同步 DO、Mapper、Service、Controller、接口 VO。
## 7. 编码前建议执行顺序
1. 先对齐 `02-产品管理_业务设计.md``02-产品管理_SQL已确认口径.md`
2. 再对齐 `rdms_biz_audit_log.sql` 与已确认 SQL 口径
3. 再开始补 DO、Mapper、Service、Controller、ReqVO、RespVO
4. 最后对齐权限、审计、状态流转和接口返回字段
## 8. 编码时禁止自由发挥的点
- 不要把产品需求状态继续做成 `tinyint` 枚举字段。
- 不要把来源追溯和需求拆分混在同一组字段里。
- 不要把 `close` 再拆回独立接口。
- 不要把状态动作塞进通用编辑接口。
- 不要在本轮直接引入 Flowable 落地代码。
- 不要在产品模块里扩展未确认的版本、路线图、附件等能力。

View File

@@ -0,0 +1,130 @@
# 05-产品管理 当前开发完成度清单
## 0. 文档定位
本文档只回答 3 件事:
- 当前产品管理后端已经做了什么
- 当前产品管理后端还有什么没做
- 前端现在到底能调哪一段,不能把哪一段当成已完成
说明:
- 本文档以当前代码实际状态为准,不写历史方案,不写计划性口径。
- 本文档当前只覆盖 `rdms-project/rdms-project-boot` 下的产品管理后端实现现状。
- 本文档中的“已完成”表示代码已实现并已静态核对,不表示已经执行编译、测试或联调。
## 1. 当前已完成
### 1.1 已完成的接口
当前产品主数据以下 6 个接口已完成代码实现:
- `GET /project/product/page`
- `GET /project/product/get`
- `POST /project/product/create`
- `PUT /project/product/update`
- `POST /project/product/change-status`
- `POST /project/product/delete`
### 1.2 已完成的主数据能力
围绕产品主数据,当前已完成以下后端能力:
- 产品分页查询
- 产品详情查询
- 创建产品
- 更新产品
- 产品状态变更
- 删除产品
### 1.3 已完成的服务端校验
当前已补齐以下校验:
- 产品存在性校验
- 产品编码未删除范围唯一校验
- 产品名称未删除范围唯一校验
- 产品经理用户有效性校验
- 产品编码创建后不可修改校验
- 产品状态动作必须命中 `rdms_object_status_transition` 校验
- 状态动作原因是否必填校验
- 删除时产品名称二次确认一致校验
### 1.4 已完成的状态与留痕能力
当前已补齐以下状态处理和留痕:
- 创建时默认状态写入 `active`
- 未传产品编码时由服务端自动生成编码,格式按 `CNPDYYYYNNN` 处理
- 状态变更按 `action_code` 驱动,不允许直接透传目标状态
- 状态变更后同步回写 `rdms_product.status_code`
- 状态变更后同步回写 `rdms_product.last_status_reason`
- 产品状态动作写入 `rdms_product_status_log`
- 创建、编辑、状态变更、删除写入 `rdms_biz_audit_log`
### 1.5 已补齐的支撑代码
当前已补齐以下代码支撑:
- 产品域错误码常量
- `rdms_biz_audit_log` 对应 DO / Mapper
- `rdms_product_status_log` 对应 DO / Mapper
- `ProductMapper` 中产品编码前缀查询能力
- `ObjectStatusTransitionMapper` 中仅按启用流转配置查询
## 2. 当前未完成
以下内容当前还没有开发完成,不能视为“产品管理已完成”:
- 产品团队
- 产品需求
- 关联项目
- 最近动态 / `activities`
- 产品上下文 / `context`
- 对象级导航与按钮权限
- 产品团队维护时的 `rdms_user_object_role` 动态写入
- 团队维护引起的产品经理关系同步
## 3. 当前已确认不做
以下内容已按当前口径确认,本阶段不做,不再视为当前主数据闭环缺口:
- 创建产品时不写 `rdms_user_object_role`
- `rdms_user_object_role` 由后续产品团队维护时动态落库
- `pause` / `archive` / `abandon` / `delete` 当前不做关联项目、执行、任务阻塞校验
## 4. 前端现在可联调范围
前端当前可以开始联调的范围,仅限“产品主数据最小闭环”:
- 产品列表
- 产品详情
- 新建产品
- 编辑产品
- 产品状态变更
- 删除产品
前端当前不应开始联调整个“产品管理”模块,尤其不应把以下内容当成可用:
- 产品团队
- 产品需求
- 关联项目
- 最近动态
- 产品上下文能力
## 5. 当前结论
当前状态不是“产品管理开发完毕”,而是:
- 产品主数据最小闭环已完成代码实现
- 整个产品管理仍有明显未完成范围
- 前端现在可以先调产品主数据 6 个接口
联调前仍需单独确认权限是否齐备,当前主数据接口涉及权限码:
- `project:product:query`
- `project:product:create`
- `project:product:update`
- `project:product:status`
- `project:product:delete`

View File

@@ -0,0 +1,536 @@
# 06-产品主数据 API 接口文档
## 0. 文档说明
本文档用于提供“产品主数据最小闭环”当前已完成接口的标准联调口径,供前端直接调试。
当前文档只覆盖以下 6 个接口:
- `GET /project/product/page`
- `GET /project/product/get`
- `POST /project/product/create`
- `PUT /project/product/update`
- `POST /project/product/change-status`
- `POST /project/product/delete`
说明:
- 本文档以当前代码实现为准。
- 本文档不覆盖产品团队、产品需求、关联项目、最近动态、产品上下文等未完成能力。
- 本文档中的返回示例为标准结构示例,不代表数据库中的真实数据。
- 当前仅做静态对齐,未执行编译、启动和联调验证。
## 1. 接口基础信息
### 1.1 访问前缀
当前 Controller 暴露前缀为:
```text
/project/product
```
### 1.2 认证与权限
默认沿用当前系统 OAuth2 / Token 认证链路。
请求头建议:
```http
Authorization: Bearer {accessToken}
Content-Type: application/json
```
各接口所需权限如下:
| 接口 | 权限码 |
|---|---|
| `GET /project/product/page` | `project:product:query` |
| `GET /project/product/get` | `project:product:query` |
| `POST /project/product/create` | `project:product:create` |
| `PUT /project/product/update` | `project:product:update` |
| `POST /project/product/change-status` | `project:product:status` |
| `POST /project/product/delete` | `project:product:delete` |
### 1.3 统一返回结构
所有接口统一返回 `CommonResult<T>`
```json
{
"code": 0,
"msg": "",
"data": {}
}
```
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | `number` | 业务返回码,成功固定为 `0` |
| `msg` | `string` | 返回消息,成功时通常为空字符串 |
| `data` | `object / array / boolean / number / null` | 业务数据 |
### 1.4 分页返回结构
分页接口 `data` 统一为:
```json
{
"total": 1,
"list": []
}
```
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | `number` | 总记录数 |
| `list` | `array` | 当前页数据列表 |
### 1.5 日期时间格式
当前接口中的日期时间字段统一按下面格式处理:
```text
yyyy-MM-dd HH:mm:ss
```
例如:
```text
2026-04-18 15:30:00
```
## 2. 产品对象字段说明
产品详情与分页列表当前返回字段一致,对应 `ProductRespVO`
| 字段 | 类型 | 必返 | 说明 |
|---|---|---|---|
| `id` | `number` | 是 | 产品主键 ID |
| `code` | `string` | 是 | 产品编码 |
| `directionCode` | `string` | 是 | 产品方向字典值 |
| `name` | `string` | 是 | 产品名称 |
| `managerUserId` | `number` | 是 | 产品经理用户 ID |
| `description` | `string` | 否 | 产品描述 |
| `statusCode` | `string` | 是 | 产品状态编码 |
| `lastStatusReason` | `string` | 否 | 最近一次状态动作原因 |
| `remark` | `string` | 否 | 备注 |
| `createTime` | `string` | 是 | 创建时间 |
| `updateTime` | `string` | 是 | 更新时间 |
## 3. 通用业务口径
### 3.1 产品方向
`directionCode` 使用系统字典 `rdms_product_direction``value`
当前设计文档中的推荐初始值为:
- `embedded`
- `power_electronics`
- `system_group`
### 3.2 产品编码
- 创建时 `code` 可传可不传。
- 如果创建时不传 `code`,后端自动生成,格式为 `CNPDYYYYNNN`
- 更新时不允许修改 `code`
### 3.3 产品状态
当前产品主数据接口涉及的状态编码:
- `active`
- `paused`
- `archived`
- `abandoned`
### 3.4 状态动作
`POST /project/product/change-status` 当前仅支持以下动作:
- `pause`
- `resume`
- `archive`
- `abandon`
动作驱动规则:
- 前端传 `actionCode`
- 后端按 `rdms_object_status_transition` 校验是否允许流转
- 前端不能直接传目标状态编码
### 3.5 当前编辑限制
当前代码口径下:
- `archived``abandoned` 状态不允许编辑
- `paused` 状态下,仅允许调整 `managerUserId``description``remark`
- `paused` 状态下不允许修改 `directionCode``name`
## 4. 接口明细
## 4.1 获取产品分页
### 接口信息
| 项目 | 内容 |
|---|---|
| 请求方式 | `GET` |
| 接口路径 | `/project/product/page` |
| 权限码 | `project:product:query` |
### 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `pageNo` | query | `number` | 是 | 页码,从 `1` 开始 |
| `pageSize` | query | `number` | 是 | 每页条数,最大 `200` |
| `keyword` | query | `string` | 否 | 关键词,匹配产品编码或产品名称 |
| `directionCode` | query | `string` | 否 | 产品方向字典值 |
| `managerUserId` | query | `number` | 否 | 产品经理用户 ID |
| `statusCode` | query | `string` | 否 | 产品状态编码 |
| `updateTime` | query | `string[]` | 否 | 更新时间区间,建议传两个同名参数 |
`updateTime` 示例:
```text
/project/product/page?pageNo=1&pageSize=10&updateTime=2026-04-01 00:00:00&updateTime=2026-04-30 23:59:59
```
### 请求示例
```http
GET /project/product/page?pageNo=1&pageSize=10&keyword=RDMS&statusCode=active
```
### 成功返回示例
```json
{
"code": 0,
"msg": "",
"data": {
"total": 2,
"list": [
{
"id": 3200000000001,
"code": "CNPD2026001",
"directionCode": "embedded",
"name": "RDMS产品平台",
"managerUserId": 1024,
"description": "面向研发管理的一体化产品",
"statusCode": "active",
"lastStatusReason": null,
"remark": "首批试点产品",
"createTime": "2026-04-18 09:30:00",
"updateTime": "2026-04-18 09:30:00"
}
]
}
}
```
## 4.2 获取产品详情
### 接口信息
| 项目 | 内容 |
|---|---|
| 请求方式 | `GET` |
| 接口路径 | `/project/product/get` |
| 权限码 | `project:product:query` |
### 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `id` | query | `number` | 是 | 产品 ID |
### 请求示例
```http
GET /project/product/get?id=3200000000001
```
### 成功返回示例
```json
{
"code": 0,
"msg": "",
"data": {
"id": 3200000000001,
"code": "CNPD2026001",
"directionCode": "embedded",
"name": "RDMS产品平台",
"managerUserId": 1024,
"description": "面向研发管理的一体化产品",
"statusCode": "active",
"lastStatusReason": null,
"remark": "首批试点产品",
"createTime": "2026-04-18 09:30:00",
"updateTime": "2026-04-18 09:30:00"
}
}
```
## 4.3 创建产品
### 接口信息
| 项目 | 内容 |
|---|---|
| 请求方式 | `POST` |
| 接口路径 | `/project/product/create` |
| 权限码 | `project:product:create` |
### 请求体字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | `number` | 否 | 创建时不需要传 |
| `code` | `string` | 否 | 产品编码;为空时后端自动生成 |
| `directionCode` | `string` | 是 | 产品方向字典值 |
| `name` | `string` | 是 | 产品名称 |
| `managerUserId` | `number` | 是 | 产品经理用户 ID |
| `description` | `string` | 否 | 产品描述 |
| `remark` | `string` | 否 | 备注 |
### 请求示例
```json
{
"directionCode": "embedded",
"name": "RDMS产品平台",
"managerUserId": 1024,
"description": "面向研发管理的一体化产品",
"remark": "首批试点产品"
}
```
### 成功返回示例
```json
{
"code": 0,
"msg": "",
"data": 3200000000001
}
```
### 当前服务端规则
- `name` 必填,长度最大 `128`
- `directionCode` 必填,长度最大 `32`
- `managerUserId` 必填
- `code` 最大长度 `64`
- `remark` 最大长度 `500`
- 产品编码未删除范围唯一
- 产品名称未删除范围唯一
- 产品经理必须是有效用户
- 创建成功后默认状态为 `active`
## 4.4 更新产品
### 接口信息
| 项目 | 内容 |
|---|---|
| 请求方式 | `PUT` |
| 接口路径 | `/project/product/update` |
| 权限码 | `project:product:update` |
### 请求体字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | `number` | 是 | 产品 ID |
| `code` | `string` | 否 | 如传入,必须与原产品编码一致 |
| `directionCode` | `string` | 是 | 产品方向字典值 |
| `name` | `string` | 是 | 产品名称 |
| `managerUserId` | `number` | 是 | 产品经理用户 ID |
| `description` | `string` | 否 | 产品描述 |
| `remark` | `string` | 否 | 备注 |
### 请求示例
```json
{
"id": 3200000000001,
"code": "CNPD2026001",
"directionCode": "embedded",
"name": "RDMS产品平台",
"managerUserId": 2048,
"description": "更新后的产品描述",
"remark": "已切换负责人"
}
```
### 成功返回示例
```json
{
"code": 0,
"msg": "",
"data": true
}
```
### 当前服务端规则
- `id` 必传
- 产品必须存在
- 产品经理必须是有效用户
- 产品编码不允许修改
- 产品名称未删除范围唯一
- `archived``abandoned` 状态不允许编辑
- `paused` 状态仅允许调整 `managerUserId``description``remark`
## 4.5 变更产品状态
### 接口信息
| 项目 | 内容 |
|---|---|
| 请求方式 | `POST` |
| 接口路径 | `/project/product/change-status` |
| 权限码 | `project:product:status` |
### 请求体字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | `number` | 是 | 产品 ID |
| `actionCode` | `string` | 是 | 动作编码 |
| `reason` | `string` | 否 | 动作原因;是否必填由流转配置决定 |
### `actionCode` 当前支持值
| `actionCode` | 含义 | 当前典型流转 | 原因是否必填 |
|---|---|---|---|
| `pause` | 暂停 | `active -> paused` | 是 |
| `resume` | 恢复 | `paused -> active` | 否 |
| `archive` | 归档 | `active / paused -> archived` | 是 |
| `abandon` | 废弃 | `active / paused -> abandoned` | 是 |
### 请求示例
```json
{
"id": 3200000000001,
"actionCode": "pause",
"reason": "当前阶段资源受限,先暂停推进"
}
```
### 成功返回示例
```json
{
"code": 0,
"msg": "",
"data": true
}
```
### 当前服务端规则
- 产品必须存在
- 动作必须命中 `rdms_object_status_transition`
- 前端不能直接传目标状态
- 若当前流转配置要求必须填写原因,则 `reason` 必填
- 状态变更后会同步回写:
- `rdms_product.status_code`
- `rdms_product.last_status_reason`
- 状态变更后会写入:
- `rdms_product_status_log`
- `rdms_biz_audit_log`
## 4.6 删除产品
### 接口信息
| 项目 | 内容 |
|---|---|
| 请求方式 | `POST` |
| 接口路径 | `/project/product/delete` |
| 权限码 | `project:product:delete` |
### 请求体字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | `number` | 是 | 产品 ID |
| `productName` | `string` | 是 | 二次确认输入的产品名称 |
| `reason` | `string` | 是 | 删除原因 |
### 请求示例
```json
{
"id": 3200000000001,
"productName": "RDMS产品平台",
"reason": "产品录入错误,需重新创建"
}
```
### 成功返回示例
```json
{
"code": 0,
"msg": "",
"data": true
}
```
### 当前服务端规则
- 产品必须存在
- `productName` 必须与当前产品名称完全一致
- `reason` 必填
- 当前删除实现为逻辑删除
- 删除后会写入:
- `rdms_product_status_log`
- `rdms_biz_audit_log`
## 5. 业务错误码
当前产品主数据接口已使用的产品域错误码如下:
| 错误码 | 常量 | 含义 |
|---|---|---|
| `1008001000` | `PRODUCT_NOT_EXISTS` | 产品不存在 |
| `1008001001` | `PRODUCT_CODE_DUPLICATE` | 已存在相同产品编码 |
| `1008001002` | `PRODUCT_NAME_DUPLICATE` | 已存在相同产品名称 |
| `1008001003` | `PRODUCT_CODE_NOT_MODIFIABLE` | 产品编码创建后不允许修改 |
| `1008001004` | `PRODUCT_STATUS_ACTION_NOT_ALLOWED` | 当前状态不支持指定动作 |
| `1008001005` | `PRODUCT_STATUS_ACTION_REASON_REQUIRED` | 当前动作必须填写原因 |
| `1008001006` | `PRODUCT_DELETE_NAME_MISMATCH` | 删除确认名称与当前产品名称不一致 |
| `1008001007` | `PRODUCT_STATUS_NOT_ALLOW_EDIT` | 当前产品状态不允许编辑 |
| `1008001008` | `PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE` | 产品暂停后仅允许变更产品经理、描述和备注 |
此外还可能返回全局错误码:
| 错误码 | 含义 |
|---|---|
| `0` | 成功 |
| `400` | 请求参数不正确 |
| `401` | 账号未登录 |
| `403` | 没有该操作权限 |
| `500` | 系统异常 |
## 6. 联调注意事项
当前前端联调时请注意:
- 当前只联调产品主数据,不要把产品团队、产品需求、关联项目等能力一起接入。
- 创建产品时不写 `rdms_user_object_role`,产品团队关系留待后续团队维护接口处理。
- `pause` / `archive` / `abandon` / `delete` 当前不做关联项目、执行、任务阻塞校验。
- 若联调账号缺少权限,会直接返回 `403`
- 若产品方向字典值未准备好,创建和更新接口会触发字典校验失败。

View File

@@ -0,0 +1,287 @@
/*
产品管理初始化 SQL
说明:
1. 本文件作为当前产品管理唯一执行 SQL。
2. 产品方向 `direction_code` 统一存系统字典 `value`;系统字典数据本文件不重复创建。
3. 产品与产品需求状态统一走状态编码模型。
4. 产品需求当前先按已确认状态集落库;补齐流转动作码 `start_execution`、`accept`。
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for rdms_biz_audit_log
-- ----------------------------
DROP TABLE IF EXISTS `rdms_biz_audit_log`;
CREATE TABLE `rdms_biz_audit_log` (
`id` bigint NOT NULL COMMENT '主键ID',
`biz_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '业务对象类型product、product_requirement、rdms_user_object_role、project、project_requirement、execution、task',
`biz_id` bigint NOT NULL COMMENT '业务对象ID',
`action_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '动作类型create、update、change_manager、add_member、remove_member、claim、split、dispatch、reject、cancel、close、start_execution、accept、export',
`from_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原状态',
`to_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '目标状态',
`field_changes` json NULL COMMENT '关键字段变更摘要(用于负责人变更、成员调整等)',
`reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '动作原因或说明',
`operator_user_id` bigint NOT NULL COMMENT '操作人用户ID',
`operator_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作人名称快照',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_rdms_biz_audit_log_biz_deleted`(`biz_type` ASC, `biz_id` ASC, `deleted` ASC) USING BTREE COMMENT '业务对象索引',
INDEX `idx_rdms_biz_audit_log_action_deleted`(`action_type` ASC, `deleted` ASC) USING BTREE COMMENT '动作类型索引',
INDEX `idx_rdms_biz_audit_log_operator_deleted`(`operator_user_id` ASC, `deleted` ASC) USING BTREE COMMENT '操作人索引',
INDEX `idx_rdms_biz_audit_log_create_time`(`create_time` ASC) USING BTREE COMMENT '创建时间索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'RDMS通用业务审计日志表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for rdms_object_status_model
-- ----------------------------
DROP TABLE IF EXISTS `rdms_object_status_model`;
CREATE TABLE `rdms_object_status_model` (
`id` bigint NOT NULL COMMENT '主键ID',
`object_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '对象类型product、project、product_requirement、project_requirement、execution、task',
`status_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '状态编码',
`status_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '状态名称',
`sort` int NOT NULL DEFAULT 0 COMMENT '排序值',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '配置状态0启用 1停用',
`initial_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否初始状态',
`terminal_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否终态',
`allow_edit` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否允许编辑对象主数据',
`allow_create_project` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否允许新建项目',
`allow_create_requirement` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否允许新增需求',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_rdms_object_status_model_object_status_deleted`(`object_type` ASC, `status_code` ASC, `deleted` ASC) USING BTREE COMMENT '对象状态编码未删除范围唯一',
INDEX `idx_rdms_object_status_model_object_sort_deleted`(`object_type` ASC, `sort` ASC, `deleted` ASC) USING BTREE COMMENT '对象状态排序索引',
INDEX `idx_rdms_object_status_model_object_terminal_deleted`(`object_type` ASC, `terminal_flag` ASC, `deleted` ASC) USING BTREE COMMENT '对象终态索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'RDMS对象状态模型表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of rdms_object_status_model
-- ----------------------------
INSERT INTO `rdms_object_status_model` VALUES (3100000001001, 'product', 'active', '启用', 10, 0, b'1', b'0', b'1', b'1', b'1', '产品正常可用状态', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001002, 'product', 'paused', '暂停', 20, 0, b'0', b'0', b'1', b'0', b'0', '受环境或资源限制临时暂停推进', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001003, 'product', 'archived', '归档', 30, 0, b'0', b'1', b'0', b'0', b'0', '历史留存,只读为主', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001004, 'product', 'abandoned', '废弃', 40, 0, b'0', b'1', b'0', b'0', b'0', '确认不再继续推进', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001201, 'product_requirement', 'pending_confirm', '待确认', 10, 0, b'1', b'0', b'0', b'0', b'0', '工单流转到产品侧后的初始状态', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001202, 'product_requirement', 'pending_process', '待处理', 20, 0, b'1', b'0', b'1', b'0', b'0', '手工新增后的默认状态', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001203, 'product_requirement', 'pending_review', '待评审', 30, 0, b'0', b'0', b'1', b'0', b'0', '待产品侧完成业务评审判断', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001204, 'product_requirement', 'pending_dispatch', '待分流', 40, 0, b'0', b'0', b'1', b'0', b'0', '需求成立,等待明确承接方向', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001205, 'product_requirement', 'dispatched', '已分流', 50, 0, b'0', b'0', b'1', b'0', b'0', '已明确承接方向', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001206, 'product_requirement', 'in_progress', '实施中', 60, 0, b'0', b'0', b'1', b'0', b'0', '承接项目已进入正式执行', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001207, 'product_requirement', 'accepted', '已验收', 70, 0, b'0', b'0', b'1', b'0', b'0', '承接结果已完成验收', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001208, 'product_requirement', 'closed', '已关闭', 80, 0, b'0', b'1', b'0', b'0', b'0', '生命周期闭环完成', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001209, 'product_requirement', 'rejected', '已拒绝', 90, 0, b'0', b'1', b'0', b'0', b'0', '需求确认不成立或产品侧不接收', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_model` VALUES (3100000001210, 'product_requirement', 'canceled', '已取消', 100, 0, b'0', b'1', b'0', b'0', b'0', '推进过程中终止', '', NOW(), '', NOW(), b'0');
-- ----------------------------
-- Table structure for rdms_object_status_transition
-- ----------------------------
DROP TABLE IF EXISTS `rdms_object_status_transition`;
CREATE TABLE `rdms_object_status_transition` (
`id` bigint NOT NULL COMMENT '主键ID',
`object_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '对象类型product、project、product_requirement、project_requirement、execution、task',
`action_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '动作编码pause、resume、archive、abandon、claim、to_review、to_dispatch、dispatch、start_execution、accept、reject、cancel、close',
`action_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '动作名称',
`from_status_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '起始状态编码',
`to_status_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '目标状态编码',
`need_reason` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否必须填写原因1必须 0非必须',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '配置状态0启用 1停用',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_rdms_object_status_transition_object_from_action_deleted`(`object_type` ASC, `from_status_code` ASC, `action_code` ASC, `deleted` ASC) USING BTREE COMMENT '对象起始状态动作未删除范围唯一',
INDEX `idx_rdms_object_status_transition_object_from_deleted`(`object_type` ASC, `from_status_code` ASC, `status` ASC, `deleted` ASC) USING BTREE COMMENT '对象起始状态流转索引',
INDEX `idx_rdms_object_status_transition_object_to_deleted`(`object_type` ASC, `to_status_code` ASC, `status` ASC, `deleted` ASC) USING BTREE COMMENT '对象目标状态流转索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'RDMS对象状态流转表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of rdms_object_status_transition
-- ----------------------------
INSERT INTO `rdms_object_status_transition` VALUES (3100000001101, 'product', 'pause', '暂停', 'active', 'paused', b'1', 0, '启用转暂停', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001102, 'product', 'resume', '恢复', 'paused', 'active', b'0', 0, '暂停恢复启用', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001103, 'product', 'archive', '归档', 'active', 'archived', b'1', 0, '启用转归档', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001104, 'product', 'archive', '归档', 'paused', 'archived', b'1', 0, '暂停转归档', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001105, 'product', 'abandon', '废弃', 'active', 'abandoned', b'1', 0, '启用转废弃', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001106, 'product', 'abandon', '废弃', 'paused', 'abandoned', b'1', 0, '暂停转废弃', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001301, 'product_requirement', 'claim', '认领', 'pending_confirm', 'pending_process', b'0', 0, '待确认转待处理', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001302, 'product_requirement', 'reject', '拒绝', 'pending_confirm', 'rejected', b'1', 0, '待确认转已拒绝', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001303, 'product_requirement', 'cancel', '取消', 'pending_confirm', 'canceled', b'1', 0, '待确认转已取消', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001304, 'product_requirement', 'to_review', '转待评审', 'pending_process', 'pending_review', b'0', 0, '待处理转待评审', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001305, 'product_requirement', 'to_dispatch', '转待分流', 'pending_process', 'pending_dispatch', b'0', 0, '待处理转待分流', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001306, 'product_requirement', 'reject', '拒绝', 'pending_process', 'rejected', b'1', 0, '待处理转已拒绝', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001307, 'product_requirement', 'cancel', '取消', 'pending_process', 'canceled', b'1', 0, '待处理转已取消', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001308, 'product_requirement', 'to_dispatch', '转待分流', 'pending_review', 'pending_dispatch', b'0', 0, '待评审转待分流', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001309, 'product_requirement', 'reject', '拒绝', 'pending_review', 'rejected', b'1', 0, '待评审转已拒绝', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001310, 'product_requirement', 'cancel', '取消', 'pending_review', 'canceled', b'1', 0, '待评审转已取消', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001311, 'product_requirement', 'dispatch', '分流', 'pending_dispatch', 'dispatched', b'0', 0, '待分流转已分流', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001312, 'product_requirement', 'cancel', '取消', 'pending_dispatch', 'canceled', b'1', 0, '待分流转已取消', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001313, 'product_requirement', 'start_execution', '开始实施', 'dispatched', 'in_progress', b'0', 0, '已分流转实施中', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001314, 'product_requirement', 'cancel', '取消', 'dispatched', 'canceled', b'1', 0, '已分流转已取消', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001315, 'product_requirement', 'accept', '验收', 'in_progress', 'accepted', b'0', 0, '实施中转已验收', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001316, 'product_requirement', 'cancel', '取消', 'in_progress', 'canceled', b'1', 0, '实施中转已取消', '', NOW(), '', NOW(), b'0');
INSERT INTO `rdms_object_status_transition` VALUES (3100000001317, 'product_requirement', 'close', '关闭', 'accepted', 'closed', b'1', 0, '已验收转已关闭', '', NOW(), '', NOW(), b'0');
-- ----------------------------
-- Table structure for rdms_product
-- ----------------------------
DROP TABLE IF EXISTS `rdms_product`;
CREATE TABLE `rdms_product` (
`id` bigint NOT NULL COMMENT '主键ID',
`code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '产品编码格式CNPDYYYYNNN支持手工录入或系统自动生成',
`direction_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '产品方向字典值system_dict_data.value推荐字典类型 rdms_product_direction',
`status_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active' COMMENT '产品状态编码(引用 rdms_object_status_model.status_codeobject_type = product',
`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '产品名称',
`manager_user_id` bigint NOT NULL COMMENT '当前产品经理用户ID冗余读模型字段权威来源仍为 rdms_user_object_role',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '产品描述',
`last_status_reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最近一次状态动作原因',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_rdms_product_code_deleted`(`code` ASC, `deleted` ASC) USING BTREE COMMENT '产品编码未删除范围唯一',
UNIQUE INDEX `uk_rdms_product_name_deleted`(`name` ASC, `deleted` ASC) USING BTREE COMMENT '产品名称未删除范围唯一',
INDEX `idx_rdms_product_direction_status_code_deleted`(`direction_code` ASC, `status_code` ASC, `deleted` ASC) USING BTREE COMMENT '产品方向状态索引',
INDEX `idx_rdms_product_manager_status_code_deleted`(`manager_user_id` ASC, `status_code` ASC, `deleted` ASC) USING BTREE COMMENT '产品经理状态索引',
INDEX `idx_rdms_product_status_code_deleted`(`status_code` ASC, `deleted` ASC) USING BTREE COMMENT '产品状态索引',
INDEX `idx_rdms_product_update_time`(`update_time` ASC) USING BTREE COMMENT '更新时间索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '产品主表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for rdms_product_rd_order
-- ----------------------------
DROP TABLE IF EXISTS `rdms_product_rd_order`;
CREATE TABLE `rdms_product_rd_order` (
`id` bigint NOT NULL COMMENT '主键ID',
`product_id` bigint NOT NULL COMMENT '产品ID',
`order_year` int NOT NULL COMMENT '研发令号年度',
`rd_order_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '研发令号',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态0有效 1失效',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_rdms_product_rd_order_product_year_deleted`(`product_id` ASC, `order_year` ASC, `deleted` ASC) USING BTREE COMMENT '同一产品同一年度未删除范围唯一',
INDEX `idx_rdms_product_rd_order_product_status_deleted`(`product_id` ASC, `status` ASC, `deleted` ASC) USING BTREE COMMENT '产品研发令号状态索引',
INDEX `idx_rdms_product_rd_order_no_deleted`(`rd_order_no` ASC, `deleted` ASC) USING BTREE COMMENT '研发令号检索索引',
INDEX `idx_rdms_product_rd_order_year_deleted`(`order_year` ASC, `deleted` ASC) USING BTREE COMMENT '研发令年度索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '产品研发令号表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for rdms_product_requirement
-- ----------------------------
DROP TABLE IF EXISTS `rdms_product_requirement`;
CREATE TABLE `rdms_product_requirement` (
`id` bigint NOT NULL COMMENT '主键ID',
`product_id` bigint NOT NULL COMMENT '所属产品ID',
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '需求标题',
`category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '需求分类',
`source_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '需求来源类型manual、work_order',
`source_biz_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '来源业务类型work_order',
`source_biz_id` bigint NULL DEFAULT NULL COMMENT '来源业务ID',
`source_biz_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '来源业务编号',
`root_requirement_id` bigint NULL DEFAULT NULL COMMENT '源头需求ID同一来源链路根节点',
`parent_requirement_id` bigint NULL DEFAULT NULL COMMENT '直接父需求ID拆分子需求回指父需求',
`priority` tinyint NOT NULL DEFAULT 1 COMMENT '优先级0低 1中 2高 3紧急',
`status_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending_process' COMMENT '需求状态编码(引用 rdms_object_status_model.status_codeobject_type = product_requirement',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '需求描述',
`proposer_id` bigint NOT NULL COMMENT '提出人用户ID',
`current_handler_user_id` bigint NULL DEFAULT NULL COMMENT '当前处理人用户ID',
`implement_project_id` bigint NULL DEFAULT NULL COMMENT '默认实现项目ID',
`terminal_action_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '终态动作编码reject、cancel、close',
`terminal_reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '终态原因',
`terminal_time` datetime NULL DEFAULT NULL COMMENT '终态时间',
`sort` int NOT NULL DEFAULT 0 COMMENT '排序值',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_rdms_product_requirement_product_status_deleted`(`product_id` ASC, `status_code` ASC, `deleted` ASC) USING BTREE COMMENT '产品需求状态索引',
INDEX `idx_rdms_product_requirement_product_source_deleted`(`product_id` ASC, `source_type` ASC, `deleted` ASC) USING BTREE COMMENT '产品需求来源索引',
INDEX `idx_rdms_product_requirement_product_priority_deleted`(`product_id` ASC, `priority` ASC, `deleted` ASC) USING BTREE COMMENT '产品需求优先级索引',
INDEX `idx_rdms_product_requirement_source_biz_deleted`(`source_biz_type` ASC, `source_biz_id` ASC, `deleted` ASC) USING BTREE COMMENT '来源业务索引',
INDEX `idx_rdms_product_requirement_root_deleted`(`root_requirement_id` ASC, `deleted` ASC) USING BTREE COMMENT '源头需求索引',
INDEX `idx_rdms_product_requirement_parent_deleted`(`parent_requirement_id` ASC, `deleted` ASC) USING BTREE COMMENT '父需求索引',
INDEX `idx_rdms_product_requirement_handler_deleted`(`current_handler_user_id` ASC, `deleted` ASC) USING BTREE COMMENT '当前处理人索引',
INDEX `idx_rdms_product_requirement_terminal_action_deleted`(`terminal_action_code` ASC, `deleted` ASC) USING BTREE COMMENT '终态动作索引',
INDEX `idx_rdms_product_requirement_implement_project_deleted`(`implement_project_id` ASC, `deleted` ASC) USING BTREE COMMENT '默认实现项目索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '产品需求表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for rdms_product_status_log
-- ----------------------------
DROP TABLE IF EXISTS `rdms_product_status_log`;
CREATE TABLE `rdms_product_status_log` (
`id` bigint NOT NULL COMMENT '主键ID',
`product_id` bigint NOT NULL COMMENT '产品ID',
`action_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '动作类型pause、resume、archive、abandon、delete',
`from_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '变更前状态编码',
`to_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '变更后状态编码',
`reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '动作原因',
`operator_user_id` bigint NOT NULL COMMENT '操作人用户ID',
`operator_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作人名称快照',
`product_code_snapshot` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '产品编码快照',
`product_name_snapshot` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '产品名称快照',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_rdms_product_status_log_product_deleted`(`product_id` ASC, `deleted` ASC) USING BTREE COMMENT '产品状态日志索引',
INDEX `idx_rdms_product_status_log_operator_deleted`(`operator_user_id` ASC, `deleted` ASC) USING BTREE COMMENT '操作人索引',
INDEX `idx_rdms_product_status_log_create_time`(`create_time` ASC) USING BTREE COMMENT '创建时间索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '产品状态日志表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for rdms_user_object_role
-- ----------------------------
DROP TABLE IF EXISTS `rdms_user_object_role`;
CREATE TABLE `rdms_user_object_role` (
`id` bigint NOT NULL COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`object_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '对象类型product、project',
`object_id` bigint NOT NULL COMMENT '对象ID',
`role_id` bigint NOT NULL COMMENT '对象角色ID指向 system_role.id要求 scope_type = object',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态0有效 1失效成员关系是否有效的唯一判定字段',
`joined_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
`left_time` datetime NULL DEFAULT NULL COMMENT '退出时间,仅用于留痕,不参与权限判断',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_rdms_user_object_role_user_object_deleted`(`user_id` ASC, `object_type` ASC, `object_id` ASC, `deleted` ASC) USING BTREE COMMENT '用户对象关系未删除范围唯一',
INDEX `idx_rdms_user_object_role_object_status_deleted`(`object_type` ASC, `object_id` ASC, `status` ASC, `deleted` ASC) USING BTREE COMMENT '对象成员索引',
INDEX `idx_rdms_user_object_role_role_deleted`(`role_id` ASC, `deleted` ASC) USING BTREE COMMENT '对象角色索引',
INDEX `idx_rdms_user_object_role_user_deleted`(`user_id` ASC, `deleted` ASC) USING BTREE COMMENT '用户索引'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'RDMS对象成员角色关系表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;

View File

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

View File

@@ -0,0 +1,4 @@
/**
* Project API 实现包,放置对外暴露 RPC 接口的实现类
*/
package com.njcn.rdms.module.project.api;

View File

@@ -0,0 +1,4 @@
/**
* 管理端控制器包
*/
package com.njcn.rdms.module.project.controller.admin;

View File

@@ -0,0 +1,81 @@
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.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 = "更新产品")
@PreAuthorize("@ss.hasPermission('project:product:update')")
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")
@PreAuthorize("@ss.hasPermission('project:product:query')")
public CommonResult<ProductRespVO> getProduct(@RequestParam("id") Long id) {
ProductDO product = productService.getProduct(id);
return success(BeanUtils.toBean(product, ProductRespVO.class));
}
@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 = "变更产品状态")
@PreAuthorize("@ss.hasPermission('project:product:status')")
public CommonResult<Boolean> changeProductStatus(@Valid @RequestBody ProductStatusActionReqVO reqVO) {
productService.changeProductStatus(reqVO);
return success(true);
}
@PostMapping("/delete")
@Operation(summary = "删除产品")
@PreAuthorize("@ss.hasPermission('project:product:delete')")
public CommonResult<Boolean> deleteProduct(@Valid @RequestBody ProductDeleteReqVO reqVO) {
productService.deleteProduct(reqVO);
return success(true);
}
}

View File

@@ -0,0 +1,27 @@
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;
}

View File

@@ -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.project.enums.ProjectDictTypeConstants;
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 = "embedded")
@InDict(type = ProjectDictTypeConstants.PRODUCT_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;
}

View File

@@ -0,0 +1,45 @@
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 = "embedded")
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 = "备注", example = "预留")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,44 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import com.njcn.rdms.framework.dict.validation.InDict;
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
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 = "embedded")
@NotBlank(message = "产品方向不能为空")
@Size(max = 32, message = "产品方向长度不能超过32个字符")
@InDict(type = ProjectDictTypeConstants.PRODUCT_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;
@Schema(description = "备注", example = "预留")
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
}

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 应用端控制器包
*/
package com.njcn.rdms.module.project.controller.app;

View File

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

View File

@@ -0,0 +1,4 @@
/**
* DTO、VO、DO 等对象转换包
*/
package com.njcn.rdms.module.project.convert;

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 数据对象包
*/
package com.njcn.rdms.module.project.dal.dataobject;

View File

@@ -0,0 +1,55 @@
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;
/**
* 备注
*/
private String remark;
}

View File

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

View File

@@ -0,0 +1,67 @@
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 Boolean allowCreateProject;
/**
* 是否允许新增需求
*/
private Boolean allowCreateRequirement;
/**
* 备注
*/
private String remark;
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
/**
* MyBatis Mapper 包
*/
package com.njcn.rdms.module.project.dal.mysql;

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
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 List<ObjectStatusModelDO> selectListByObjectType(String objectType) {
return selectList(new LambdaQueryWrapperX<ObjectStatusModelDO>()
.eq(ObjectStatusModelDO::getObjectType, objectType)
.orderByAsc(ObjectStatusModelDO::getSort));
}
}

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 持久层包
*/
package com.njcn.rdms.module.project.dal;

View File

@@ -0,0 +1,13 @@
package com.njcn.rdms.module.project.framework.rpc.config;
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})
public class RpcConfiguration {
}

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 服务层包
*/
package com.njcn.rdms.module.project.service;

View File

@@ -0,0 +1,60 @@
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.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.dal.dataobject.product.ProductDO;
/**
* 产品 Service 接口
*/
public interface ProductService {
/**
* 创建产品
*
* @param createReqVO 创建请求
* @return 产品编号
*/
Long createProduct(ProductSaveReqVO createReqVO);
/**
* 更新产品
*
* @param updateReqVO 更新请求
*/
void updateProduct(ProductSaveReqVO updateReqVO);
/**
* 获取产品详情
*
* @param id 产品编号
* @return 产品信息
*/
ProductDO getProduct(Long id);
/**
* 获取产品分页
*
* @param pageReqVO 分页请求
* @return 分页结果
*/
PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO);
/**
* 变更产品状态
*
* @param reqVO 状态动作请求
*/
void changeProductStatus(ProductStatusActionReqVO reqVO);
/**
* 删除产品
*
* @param reqVO 删除请求
*/
void deleteProduct(ProductDeleteReqVO reqVO);
}

View File

@@ -0,0 +1,347 @@
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.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.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.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
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 ProductServiceImpl implements ProductService {
private static final String PRODUCT_OBJECT_TYPE = "product";
private static final String PRODUCT_ACTIVE_STATUS = "active";
private static final String PRODUCT_PAUSED_STATUS = "paused";
private static final String PRODUCT_ARCHIVED_STATUS = "archived";
private static final String PRODUCT_ABANDONED_STATUS = "abandoned";
private static final String PRODUCT_CREATE_ACTION = "create";
private static final String PRODUCT_UPDATE_ACTION = "update";
private static final String PRODUCT_DELETE_ACTION = "delete";
private static final String PRODUCT_CODE_PREFIX = "CNPD";
@Resource
private ProductMapper productMapper;
@Resource
private ProductStatusLogMapper productStatusLogMapper;
@Resource
private BizAuditLogMapper bizAuditLogMapper;
@Resource
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
@Resource
private AdminUserApi adminUserApi;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createProduct(ProductSaveReqVO createReqVO) {
validateCreateReqVO(createReqVO);
validateManagerUser(createReqVO.getManagerUserId());
ProductDO product = new ProductDO();
product.setCode(generateProductCode(createReqVO.getCode()));
product.setDirectionCode(createReqVO.getDirectionCode());
product.setStatusCode(PRODUCT_ACTIVE_STATUS);
product.setName(createReqVO.getName().trim());
product.setManagerUserId(createReqVO.getManagerUserId());
product.setDescription(normalizeNullableText(createReqVO.getDescription()));
product.setRemark(normalizeNullableText(createReqVO.getRemark()));
productMapper.insert(product);
writeBizAuditLog(product, PRODUCT_CREATE_ACTION, null, PRODUCT_ACTIVE_STATUS,
buildFieldChanges(null, product), null);
return product.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProduct(ProductSaveReqVO updateReqVO) {
if (updateReqVO.getId() == null) {
throw invalidParamException("产品编号不能为空");
}
ProductDO product = validateProductExists(updateReqVO.getId());
validateManagerUser(updateReqVO.getManagerUserId());
validateProductCodeUnchanged(product, updateReqVO.getCode());
validateProductEditable(product, updateReqVO);
validateProductNameUnique(updateReqVO.getId(), updateReqVO.getName());
ProductDO before = BeanUtils.toBean(product, ProductDO.class);
product.setDirectionCode(updateReqVO.getDirectionCode());
product.setName(updateReqVO.getName().trim());
product.setManagerUserId(updateReqVO.getManagerUserId());
product.setDescription(normalizeNullableText(updateReqVO.getDescription()));
product.setRemark(normalizeNullableText(updateReqVO.getRemark()));
productMapper.updateById(product);
writeBizAuditLog(product, PRODUCT_UPDATE_ACTION, product.getStatusCode(), product.getStatusCode(),
buildFieldChanges(before, product), null);
}
@Override
public ProductDO getProduct(Long id) {
return validateProductExists(id);
}
@Override
public PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO) {
return productMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void changeProductStatus(ProductStatusActionReqVO reqVO) {
ProductDO product = validateProductExists(reqVO.getId());
String actionCode = reqVO.getActionCode().trim();
ObjectStatusTransitionDO transition = validateProductTransition(product.getStatusCode(), actionCode);
String reason = normalizeNullableText(reqVO.getReason());
validateTransitionReason(transition, reason);
String fromStatus = product.getStatusCode();
String toStatus = transition.getToStatusCode();
product.setStatusCode(toStatus);
product.setLastStatusReason(reason);
productMapper.updateById(product);
writeProductStatusLog(product, actionCode, fromStatus, toStatus, reason);
writeBizAuditLog(product, actionCode, fromStatus, toStatus, null, reason);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteProduct(ProductDeleteReqVO reqVO) {
ProductDO product = validateProductExists(reqVO.getId());
if (!Objects.equals(product.getName(), reqVO.getProductName().trim())) {
throw exception(ErrorCodeConstants.PRODUCT_DELETE_NAME_MISMATCH);
}
String reason = reqVO.getReason().trim();
String fromStatus = product.getStatusCode();
productMapper.deleteById(reqVO.getId());
writeProductStatusLog(product, PRODUCT_DELETE_ACTION, fromStatus, null, reason);
writeBizAuditLog(product, PRODUCT_DELETE_ACTION, fromStatus, null, null, reason);
}
@VisibleForTesting
void validateCreateReqVO(ProductSaveReqVO createReqVO) {
validateProductCodeUnique(null, createReqVO.getCode());
validateProductNameUnique(null, createReqVO.getName());
}
@VisibleForTesting
ProductDO validateProductExists(Long id) {
if (id == null) {
throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
}
ProductDO product = productMapper.selectById(id);
if (product == null) {
throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
}
return product;
}
@VisibleForTesting
void validateProductCodeUnique(Long id, String code) {
if (!StringUtils.hasText(code)) {
return;
}
String normalizedCode = code.trim();
ProductDO product = productMapper.selectByCode(normalizedCode);
if (product == null) {
return;
}
if (id == null || !product.getId().equals(id)) {
throw exception(ErrorCodeConstants.PRODUCT_CODE_DUPLICATE, normalizedCode);
}
}
@VisibleForTesting
void validateProductNameUnique(Long id, String name) {
String normalizedName = name.trim();
ProductDO product = productMapper.selectByName(normalizedName);
if (product == null) {
return;
}
if (id == null || !product.getId().equals(id)) {
throw exception(ErrorCodeConstants.PRODUCT_NAME_DUPLICATE, normalizedName);
}
}
@VisibleForTesting
void validateManagerUser(Long managerUserId) {
adminUserApi.validateUser(managerUserId);
}
@VisibleForTesting
void validateProductCodeUnchanged(ProductDO product, String code) {
if (!StringUtils.hasText(code)) {
return;
}
if (!Objects.equals(product.getCode(), code.trim())) {
throw exception(ErrorCodeConstants.PRODUCT_CODE_NOT_MODIFIABLE);
}
}
@VisibleForTesting
void validateProductEditable(ProductDO product, ProductSaveReqVO updateReqVO) {
if (PRODUCT_ARCHIVED_STATUS.equals(product.getStatusCode())
|| PRODUCT_ABANDONED_STATUS.equals(product.getStatusCode())) {
throw exception(ErrorCodeConstants.PRODUCT_STATUS_NOT_ALLOW_EDIT);
}
if (!PRODUCT_PAUSED_STATUS.equals(product.getStatusCode())) {
return;
}
if (!Objects.equals(product.getDirectionCode(), updateReqVO.getDirectionCode())
|| !Objects.equals(product.getName(), updateReqVO.getName().trim())) {
throw exception(ErrorCodeConstants.PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE);
}
}
@VisibleForTesting
ObjectStatusTransitionDO validateProductTransition(String fromStatusCode, String actionCode) {
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
.selectByObjectTypeAndFromStatusAndAction(PRODUCT_OBJECT_TYPE, fromStatusCode, actionCode);
if (transition == null) {
throw exception(ErrorCodeConstants.PRODUCT_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.PRODUCT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
}
}
private String generateProductCode(String code) {
String normalizedCode = normalizeNullableText(code);
if (StringUtils.hasText(normalizedCode)) {
validateProductCodeUnique(null, normalizedCode);
return normalizedCode;
}
String year = String.valueOf(LocalDate.now().getYear());
String codePrefix = PRODUCT_CODE_PREFIX + year;
int nextSequence = 1;
for (ProductDO product : productMapper.selectListByCodePrefix(codePrefix)) {
String existedCode = product.getCode();
if (!StringUtils.hasText(existedCode) || !existedCode.matches(codePrefix + "\\d{3}")) {
continue;
}
nextSequence = Integer.parseInt(existedCode.substring(codePrefix.length())) + 1;
break;
}
if (nextSequence > 999) {
throw invalidParamException("{} 年产品自动编码序号已用尽", year);
}
String generatedCode = codePrefix + String.format("%03d", nextSequence);
validateProductCodeUnique(null, generatedCode);
return generatedCode;
}
private void writeProductStatusLog(ProductDO product, String actionType, String fromStatus,
String toStatus, String reason) {
ProductStatusLogDO statusLog = new ProductStatusLogDO();
statusLog.setProductId(product.getId());
statusLog.setActionType(actionType);
statusLog.setFromStatus(fromStatus);
statusLog.setToStatus(toStatus);
statusLog.setReason(defaultText(reason));
statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
statusLog.setProductCodeSnapshot(product.getCode());
statusLog.setProductNameSnapshot(product.getName());
productStatusLogMapper.insert(statusLog);
}
private void writeBizAuditLog(ProductDO product, String actionType, String fromStatus, String toStatus,
String fieldChanges, String reason) {
BizAuditLogDO auditLog = new BizAuditLogDO();
auditLog.setBizType(PRODUCT_OBJECT_TYPE);
auditLog.setBizId(product.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);
}
private String buildFieldChanges(ProductDO before, ProductDO after) {
Map<String, Object> fieldChanges = new LinkedHashMap<>();
appendFieldChange(fieldChanges, "code", valueOf(before, ProductDO::getCode), valueOf(after, ProductDO::getCode));
appendFieldChange(fieldChanges, "directionCode", valueOf(before, ProductDO::getDirectionCode),
valueOf(after, ProductDO::getDirectionCode));
appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProductDO::getStatusCode),
valueOf(after, ProductDO::getStatusCode));
appendFieldChange(fieldChanges, "name", valueOf(before, ProductDO::getName), valueOf(after, ProductDO::getName));
appendFieldChange(fieldChanges, "managerUserId", valueOf(before, ProductDO::getManagerUserId),
valueOf(after, ProductDO::getManagerUserId));
appendFieldChange(fieldChanges, "description", valueOf(before, ProductDO::getDescription),
valueOf(after, ProductDO::getDescription));
appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, ProductDO::getLastStatusReason),
valueOf(after, ProductDO::getLastStatusReason));
appendFieldChange(fieldChanges, "remark", valueOf(before, ProductDO::getRemark), valueOf(after, ProductDO::getRemark));
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
}
private <T> T valueOf(ProductDO product, Function<ProductDO, T> getter) {
return product == null ? null : getter.apply(product);
}
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 : "";
}
}

View File

@@ -0,0 +1,92 @@
#################### 注册中心 + 配置中心相关配置 ####################
spring:
cloud:
nacos:
server-addr: 192.168.1.103:18848 # Nacos 服务器地址
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 数据库相关配置 ####################
# 数据源配置项
autoconfigure:
exclude:
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 5 # 初始连接数
min-idle: 10 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 60000 # 配置获取连接等待超时的时间单位毫秒1 分钟)
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测检测需要关闭的空闲连接单位毫秒1 分钟)
min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间单位毫秒10 分钟)
max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间单位毫秒30 分钟)
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true # 是否开启 PreparedStatement 缓存
max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量
primary: master
datasource:
master:
url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
username: root
password: njcnpqs
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: 192.168.1.22 # 地址
port: 16379 # 端口
database: 1 # 数据库索引
# password: njcnpqs # 密码,建议生产环境开启
#################### 监控相关配置 ####################
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 *,可以开放所有端点。
# 日志文件配置
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
#################### RDMS 相关配置 ####################
# RDMS 配置项,设置当前项目所有自定义的配置
rdms:
demo: true # 开启演示模式

View File

@@ -0,0 +1,98 @@
#################### 注册中心 + 配置中心相关配置 ####################
spring:
cloud:
nacos:
server-addr: 192.168.1.103:18848 # Nacos 服务器地址
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 数据库相关配置 ####################
# 数据源配置项
autoconfigure:
exclude:
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 5 # 初始连接数
min-idle: 10 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 60000 # 配置获取连接等待超时的时间单位毫秒1 分钟)
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测检测需要关闭的空闲连接单位毫秒1 分钟)
min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间单位毫秒10 分钟)
max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间单位毫秒30 分钟)
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true # 是否开启 PreparedStatement 缓存
max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量
primary: master
datasource:
master:
url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
username: root
password: njcnpqs
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: 127.0.0.1 # 地址
port: 16379 # 端口
database: 1 # 数据库索引
# password: njcnpqs # 密码,建议生产环境开启
#################### 监控相关配置 ####################
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 *,可以开放所有端点。
# 日志文件配置
logging:
level:
# 配置本模块 MyBatis Mapper 打印日志
com.njcn.rdms.module.project.dal.mysql: debug
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR
# RDMS 配置项,设置当前项目所有自定义的本地扩展配置
rdms:
env: # 多环境的配置项
tag: ${HOSTNAME}
captcha:
enable: false
security:
mock-enable: true
access-log: # 访问日志的配置项
enable: true

View File

@@ -0,0 +1,105 @@
spring:
application:
name: rdms-project-server
profiles:
active: local
main:
allow-circular-references: true # 允许循环依赖,因为项目当前沿用三层架构组织方式。
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如 Feign 等会存在重复定义的服务
config:
import:
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
# Servlet 配置
servlet:
# 文件上传相关配置项
multipart:
max-file-size: 16MB # 单个文件大小
max-request-size: 32MB # 设置总上传的文件大小
# Jackson 配置项
jackson:
serialization:
write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式,例如 1611460870401
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
fail-on-empty-beans: false # 允许序列化无属性的 Bean
# Cache 配置项
cache:
type: REDIS
redis:
time-to-live: 1h # 设置过期时间为 1 小时
data:
redis:
repositories:
enabled: false # 项目未使用到 Spring Data Redis 的 Repository所以直接禁用保证启动速度
# 热部署配置
devtools:
restart:
enabled: true
server:
port: 48082
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
--- #################### 接口文档配置 ####################
springdoc:
api-docs:
enabled: true # 1. 是否开启 Swagger 接口文档的元数据
path: /v3/api-docs
swagger-ui:
enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面
path: /swagger-ui
default-flat-param-object: true
knife4j:
enable: true
setting:
language: zh_cn
# MyBatis Plus 的配置项
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 虽然默认为 true但是还是显示指定下。
global-config:
db-config:
id-type: ASSIGN_ID # 分配 ID默认使用雪花算法
logic-delete-value: 1 # 逻辑已删除值(默认为 1
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0
banner: false # 关闭控制台的 Banner 打印
type-aliases-package: ${rdms.info.base-package}.dal.dataobject
encryptor:
password: cDHvwsYb9eyLNBHp # 加解密秘钥,生产环境务必通过 Nacos 注入,切勿硬编码
mybatis-plus-join:
banner: false # 关闭控制台的 Banner 打印
# VO 转换(数据翻译)相关
easy-trans:
is-enable-global: false # 默认禁用全局翻译,避免额外性能开销
--- #################### RDMS 相关配置 ####################
rdms:
info:
version: 1.0.0
base-package: com.njcn.rdms.module.project
web:
admin-ui:
url: https://www.baidu.com # Admin 管理后台 UI 的占位地址,联调时替换成实际前端入口
xss:
enable: false
exclude-urls:
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
swagger:
title: 项目交付域管理后台
description: 提供项目集、项目、产品、需求、任务、工单、执行等管理能力
author: RDMS
version: ${rdms.info.version}
url: https://example.com
email: dev@example.com
license: Apache 2.0
license-url: https://www.apache.org/licenses/LICENSE-2.0.html
debug: false