13 Commits

Author SHA1 Message Date
a1f5936d20 Merge remote-tracking branch 'origin/main' 2026-04-18 14:20:06 +08:00
38c69c748c feat(other): 产品基础功能提交 2026-04-18 14:19:45 +08:00
dk
5815f49a79 fix(system-boot_user): 增加用户昵称不能为空的后端校验。 2026-04-16 20:55:29 +08:00
dk
0c91f5deaa fix(system-api、boot): 给用户管理功能相关的各种需要company字段的类,新增company字段。 2026-04-16 20:29:36 +08:00
dk
67040aaf5d fix(UserManagementRelationxxx.java): 优化了一些细节,主要是汇报关系 -> 管理链路。 2026-04-15 20:56:58 +08:00
dk
8af6842809 fix(UserManagementRelationxxx.java): 优化了一些细节,主要是代码注释,带人关系 -> 汇报关系。 2026-04-15 20:48:17 +08:00
9384b2f502 feat(system): 取消角色superadmin能看到所有菜单的约定,改为实际配置实际显示 2026-04-14 18:58:26 +08:00
dk
07d07c8f5f feat(UserManagementRelationxxx.java): 改造带人关系树的构造代码。
feat(UserController.java): 新增/list-by-dept-id接口,根据部门ID获取该部门和下属部门的用户精简信息列表。
fix(AdminUserServiceImpl.java): 修复删除某用户(含批量删除)后,带人关系树构造错乱、加载不出来的问题。
2026-04-14 16:32:06 +08:00
dk
c3dd0c9802 fix(package-info.java): 增加包声明。 2026-04-13 13:44:02 +08:00
dk
21ca027f3b feat(user-management-relation): 完成带人关系后端接口(即直接管理) 2026-04-10 16:26:59 +08:00
dk
017beb1d5f 1.提交到本地 2026-04-07 11:21:18 +08:00
dk
09cba49a7d Merge remote-tracking branch 'origin/main' 2026-04-07 11:14:54 +08:00
dk
7e22f79b5f 1.修复当有用户使用某个角色时,该角色也可以被禁用的BUG
2.引入热部署依赖,配置开启热部署(热更新快捷键:Ctrl+F9)
2026-04-07 11:14:28 +08:00
112 changed files with 6950 additions and 1120 deletions

231
AGENTS.md Normal file
View File

@@ -0,0 +1,231 @@
# AGENTS.md
## 适用范围
本说明适用于以 `C:\code\gitea\rdms\cn-rdms` 为根目录的整个仓库。
描述仓库现状时,以当前代码、当前配置、当前文档中可直接验证的事实为准;除非用户明确要求,不引入历史实现、过渡方案或已废弃模型来解释当前状态。
默认回答保持精简,优先给结论、改动点和必要风险,不做过多展开;如果存在你关心但未展开的细节,由你继续追问后再补充。
## 交互原则
- 默认先给执行方案,说明目标、涉及模块、预计改动点和验证方式。
- 在用户评审并明确同意前,不直接开始实际修改、编译、测试、打包或其他执行动作。
- 是否执行由用户决定;如果用户只要求分析、审阅或出方案,就停留在分析和方案层。
## 项目概览
这是一个面向 RDMS 服务的多模块 Maven 单仓库项目。
- Java 版本17
- 构建工具Maven
- 根模块打包类型:`pom`
- Spring Boot 版本:`3.5.9`
顶层模块:
1. `rdms-system`
2. `rdms-project`
3. `rdms-framework`
4. `rdms-gateway`
当前系统域能力主要集中在 `rdms-system`RDMS 核心交付域能力主要集中在 `rdms-project`,但这只是现阶段结构,不应被理解为长期只保留这两个业务模块。
后续如果新增独立业务服务,例如项目/产品管理模块、工作流模块,应继续沿用当前仓库的模块拆分方式,而不是把所有后续业务长期堆进 `rdms-system`
## 模块说明
### `rdms-system`
当前已存在的系统业务聚合模块。
- `rdms-system/rdms-system-boot`
- 主应用模块
- 启动入口:`rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/SystemServerApplication.java`
- 主包路径:`com.njcn.rdms.module.system`
- 常见子包:`api``controller``convert``dal``framework``job``service``util``websocket`
- `rdms-system/rdms-system-api`
- 供其他服务依赖的共享 API 模块
- 包含对外 API 契约与枚举定义
说明:
- 当前权限、用户、组织、岗位、菜单、角色等系统核心能力主要落在这里。
- 如果后续只是给系统域补充新的系统子能力,可以继续在 `rdms-system` 内按现有结构扩展。
- 如果后续形成独立业务域,例如 `rdms-project``rdms-workflow`,应优先建设为新的独立业务模块,而不是默认继续塞进 `rdms-system`
### `rdms-project`
当前已存在的 RDMS 核心交付业务聚合模块。
- `rdms-project/rdms-project-boot`
- 主应用模块
- 启动入口:`rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java`
- 主包路径:`com.njcn.rdms.module.project`
- 常见子包:`api``controller``convert``dal``framework``service`
- `rdms-project/rdms-project-api`
- 供其他服务依赖的共享 API 模块
- 包含对外 API 契约与枚举定义
说明:
- 当前项目集、项目、产品、需求、任务、工单、执行等 RDMS 核心业务能力应优先落在这里。
- 需要复用用户、组织、岗位、权限等系统能力时,应通过 `rdms-system-api` 调用,不要反向依赖 `rdms-system-boot`
### `rdms-framework`
共享框架与内部 starter 模块。
- `rdms-framework/rdms-common`
- 核心通用工具与公共抽象
- 其他 `rdms-spring-boot-starter-*` 模块
- 内部 starter覆盖 `env``web``rpc``security``mybatis``redis``mq``websocket``excel``protection``test``biz-ip`
### `rdms-gateway`
Spring Cloud Gateway 网关服务。
- 启动入口:`rdms-gateway/src/main/java/com/njcn/rdms/gateway/GatewayServerApplication.java`
- 主包路径:`com.njcn.rdms.gateway`
- 常见子包:`filter``handler``jackson``route``util`
## 模块演进约束
后续新增业务能力时,先区分下面两种情况,不要混用:
1. 新增独立微服务模块,例如 `rdms-project``rdms-workflow`
2. 只是在现有 `rdms-system` 中新增一个业务子域
### 新增独立微服务模块
如果后续能力已经具备独立服务边界,应优先按下面结构建设:
```text
rdms-xxx
├─ rdms-xxx-api
└─ rdms-xxx-boot
```
约束:
-`pom.xml` 增加新的聚合模块
- `api` 模块承载对外 RPC/Feign 接口、DTO、错误码、枚举、常量
- `boot` 模块承载启动类、controller、service、dal、convert、api 实现、模块级 framework 配置和资源文件
- 包路径、`spring.application.name``ApiConstants``RpcConstants``rdms.info.base-package` 必须保持一致
- 新服务不是简单复制 `rdms-system` 的名字,而是复用它的工程骨架和分层习惯
### 在 `rdms-system` 中新增业务子域
如果只是给系统服务补一个当前阶段仍适合放在 `rdms-system` 内的子域,则继续沿用现有结构:
- `controller/admin/...``controller/app/...`
- `service/...`
- `dal/dataobject/...`
- `dal/mysql/...`
- `convert/...`
- 需要跨模块暴露时,在 `rdms-system-api` 中补 API、DTO、错误码、枚举
约束:
- 不要为了新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层语言
- 不要让外部模块直接依赖 `rdms-system-boot` 的 service 或 mapper
- 如果某项能力未来明显会演进成独立微服务,文档和实现上都要避免把它写死成只能存在于 `rdms-system`
## 代码目录
- Java 源码:`*/src/main/java`
- 资源文件:`*/src/main/resources`
- 测试代码:`*/src/test/java`
- 本地辅助脚本:`scripts/`
## 分层职责约束
### `rdms-framework`
- `rdms-framework` 承担基础能力,不承载具体业务语义。
- 除非出现框架级缺陷,或该能力明确属于全局可复用基础设施,否则不要把业务判断硬塞进 framework。
### `rdms-gateway`
- `rdms-gateway` 只负责统一入口、令牌校验、登录用户透传、路由和网关层横切逻辑。
- 不要在 gateway 层承载组织、成员、负责人、项目、产品、工作流状态流转或数据可见性这类业务语义。
### Controller 层
- Controller 负责 HTTP 暴露、参数校验、权限注解、结果封装。
- 不要在 controller 中直接编排复杂业务流程,也不要直接操作多个 mapper 拼装业务规则。
- 请求和响应对象优先沿用 `ReqVO``RespVO` 风格,不要直接把 DO 暴露给前端。
### Service 层
- 核心业务规则、事务、缓存、领域编排应落在 service 层。
- 如果是已有领域增强,优先在现有 service 下扩展,不要为了“看起来更整齐”平移整套代码。
- 不要把复杂规则散落到 controller、mapper 或 `util` 中。
### DAL 层
- 新表应有对应的 DO 和 Mapper。
- Mapper 优先继承 `BaseMapperX<T>`,不要重复写样板 CRUD。
- 查询条件优先沿用 `LambdaQueryWrapperX`、默认方法封装和现有 MyBatis Plus 风格,不要无必要回退到 XML。
- Mapper 以查询封装为主,不承担领域校验职责。
### Convert 层
- 如果某个领域已经有 `convert` 风格,则继续沿用。
- 简单场景允许直接使用 `BeanUtils`
- 不要为了统一而强推所有地方都改成 MapStruct也不要反过来把已有 convert 全部删掉。
## 认证与共享调用约束
- 默认沿用现有 OAuth2 / Token / `LoginUser` / `login-user` 透传主链,不要另造一套认证上下文体系。
- 不要额外发明 ThreadLocal、Session 或自定义 header 体系替代当前登录态恢复方式。
- 接口级权限判断默认沿用 `@PreAuthorize("@ss.hasPermission(...)")` 这条链路,不要绕开现有权限框架另起一套实现。
- 跨模块、跨服务访问能力时,优先通过对应的 `*-api` 模块定义 API、DTO、常量和枚举。
- 不要让外部模块直接依赖某个 `*-boot` 模块的 service 或 mapper。
## 数据与 SQL 约束
- 新增业务表的 DO 优先复用当前 `BaseDO` / 审计字段风格;除非表本身明确不需要逻辑删除,不要再引入另一套审计基类。
- 不要假设运行时存在自动数据库迁移;如果代码依赖新表、新字段或新索引,必须同步补齐对应 SQL 与文档说明。
- SQL 脚本应放在目标模块的 `src/main/resources/sql/...` 下,并保持可审阅、可单独执行、语义清晰。
- 变更缓存、日志、审计相关逻辑时,优先沿用现有机制,不要绕开现有登录上下文、缓存约定和审计字段填充方式。
## 注释与编码
- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。
- 不要为了省事删除原有有效注释,也不要添加无信息量的注释。
- 写入中文内容时必须保持 UTF-8 编码,并自行检查中文显示是否正常;不要用“改成英文”规避乱码问题。
## 工作规则
1. 除非任务明确要求修改共享契约或 starter否则优先进行有边界的模块内改动避免跨模块扩散。
2. 业务逻辑应放在对应业务模块的 `*-boot` 实现模块;可复用契约放在对应的 `*-api` 模块;可复用框架能力放在 `rdms-framework`
3. 除非任务本身就是环境配置调整,否则避免修改 `application-local.yaml``application-dev.yaml`
4. 将本地资源 YAML 视为可能带有机器环境差异的文件;修改前先检查 git 状态。
5. 保持既有包结构约定不变:
- 控制器放在 `controller`
- 服务层放在 `service`
- 持久层放在 `dal`
- DTO/VO 转换放在 `convert`
6. 当前系统域代码主要在 `rdms-system`RDMS 核心交付域代码主要在 `rdms-project`,但这不是永久约束;新增业务能力时,先判断应该落在现有系统域内、现有项目交付域内,还是应建设为新的 `rdms-xxx` 业务模块。
7. 新增共享能力时,优先扩展现有 `rdms-spring-boot-starter-*` 模块,不要在业务服务里重复堆配置。
8. 修改跨模块使用的 API 时,需要同时更新提供方实现和对应的 `rdms-system-api` 或对应 `rdms-xxx-api` 契约。
9. 除非用户明确要求,否则不执行任何编译、构建、测试、打包或其他会实际运行项目的命令,包括但不限于 `mvn`、启动命令和脚本。
## 测试指引
先定义验证方式,再实施修改。默认通过以下方式验证:
- 代码路径是否闭环,调用链是否与模块边界一致
- 配置项、接口契约、权限标识、路由或资源注册是否前后一致
- 改动范围是否控制在当前任务所需的最小集合内
- 受影响的文档、SQL、配置或接口说明是否需要同步更新
如果任务影响了 Spring 配置、序列化、安全、路由、RPC 契约、MyBatis 行为或跨模块 API一律明确说明哪些部分已静态检查、哪些部分尚未实际运行验证。
## 给后续 Agent 的说明
- 仓库中可能存在未提交的本地配置改动,不要覆盖与当前任务无关的编辑。
- `docs/` 目录属于当前工作上下文的一部分,不是归档材料;做架构级修改前先查阅。
- 根目录 `pom.xml` 负责统一版本和依赖对齐;涉及版本调整时,优先修改根 `pom.xml`,不要散落到子模块中。

10
pom.xml
View File

@@ -9,6 +9,7 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>rdms-system</module> <module>rdms-system</module>
<module>rdms-project</module>
<module>rdms-framework</module> <module>rdms-framework</module>
<module>rdms-gateway</module> <module>rdms-gateway</module>
</modules> </modules>
@@ -118,6 +119,15 @@
<artifactId>rdms-spring-boot-starter-websocket</artifactId> <artifactId>rdms-spring-boot-starter-websocket</artifactId>
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 热部署依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>${spring.boot.version}</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@@ -26,5 +26,17 @@ public interface RpcConstants {
*/ */
String SYSTEM_PREFIX = RPC_API_PREFIX + "/system"; String SYSTEM_PREFIX = RPC_API_PREFIX + "/system";
/**
* project 服务名
*
* 注意,需要保证和 spring.application.name 保持一致
*/
String PROJECT_NAME = "rdms-project-server";
/**
* project 服务的前缀
*/
String PROJECT_PREFIX = RPC_API_PREFIX + "/project";
} }

View File

@@ -0,0 +1 @@
package com.njcn.rdms.framework.env.core;

View File

@@ -6,10 +6,10 @@ spring:
username: # Nacos 账号 username: # Nacos 账号
password: # Nacos 密码 password: # Nacos 密码
discovery: # 【配置中心】配置项 discovery: # 【配置中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境 namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项 config: # 【注册中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境 namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 监控相关配置 #################### #################### 监控相关配置 ####################

View File

@@ -6,10 +6,10 @@ spring:
username: # Nacos 账号 username: # Nacos 账号
password: # Nacos 密码 password: # Nacos 密码
discovery: # 【配置中心】配置项 discovery: # 【配置中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境 namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项 config: # 【注册中心】配置项
namespace: 1e0fcd92-49b4-4cda-b531-828c7d36fef5 # 命名空间。这里使用 dev 开发环境 namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 监控相关配置 #################### #################### 监控相关配置 ####################

View File

@@ -49,6 +49,19 @@ spring:
uri: grayLb://rdms-system-server uri: grayLb://rdms-system-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/system/ws/** - Path=/system/ws/**
## project-server 服务
- id: project-admin-api # 路由的编号
uri: grayLb://rdms-project-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/project/**
filters:
- RewritePath=/admin-api/project/v3/api-docs, /v3/api-docs
- id: project-app-api # 路由的编号
uri: grayLb://rdms-project-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/app-api/project/**
filters:
- RewritePath=/app-api/project/v3/api-docs, /v3/api-docs
## bpm-server 服务 ## bpm-server 服务
- id: bpm-admin-api # 路由的编号 - id: bpm-admin-api # 路由的编号
uri: grayLb://rdms-bpm-server uri: grayLb://rdms-bpm-server
@@ -76,6 +89,9 @@ knife4j:
- name: system-server - name: system-server
service-name: rdms-system-server service-name: rdms-system-server
url: /admin-api/system/v3/api-docs url: /admin-api/system/v3/api-docs
- name: project-server
service-name: rdms-project-server
url: /admin-api/project/v3/api-docs
- name: bpm-server - name: bpm-server
service-name: bpm-server service-name: bpm-server
url: /admin-api/bpm/v3/api-docs url: /admin-api/bpm/v3/api-docs

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

View File

@@ -0,0 +1,49 @@
package com.njcn.rdms.module.system.api.user;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.collection.CollectionUtils;
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
import com.njcn.rdms.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@FeignClient(name = ApiConstants.NAME)
@Tag(name = "RPC 服务 - 用户管理链路")
public interface UserManagementRelationApi {
String PREFIX = ApiConstants.PREFIX + "/user-management-relation";
@GetMapping(PREFIX + "/list-by-manager")
@Operation(summary = "根据管理者用户ID获得管理链路列表")
@Parameter(name = "managerUserId", description = "管理者用户ID", example = "1", required = true)
CommonResult<List<UserManagementRelationRespDTO>> getRelationListByManagerUserId(@RequestParam("managerUserId") Long managerUserId);
@GetMapping(PREFIX + "/list-by-subordinate")
@Operation(summary = "根据被管理者用户ID获得管理链路列表")
@Parameter(name = "subordinateUserId", description = "被管理者用户ID", example = "2", required = true)
CommonResult<List<UserManagementRelationRespDTO>> getRelationListBySubordinateUserId(@RequestParam("subordinateUserId") Long subordinateUserId);
@GetMapping(PREFIX + "/list")
@Operation(summary = "获得管理链路列表")
@Parameter(name = "ids", description = "关系编号数组", example = "1,2", required = true)
CommonResult<List<UserManagementRelationRespDTO>> getRelationList(@RequestParam("ids") Collection<Long> ids);
default Map<Long, UserManagementRelationRespDTO> getRelationMap(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return MapUtil.empty();
}
List<UserManagementRelationRespDTO> list = getRelationList(ids).getData();
return CollectionUtils.convertMap(list, UserManagementRelationRespDTO::getId);
}
}

View File

@@ -14,6 +14,9 @@ public class AdminUserRespDTO implements VO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王") @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王")
private String nickname; private String nickname;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "帐号状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "帐号状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status; // 参见 CommonStatusEnum 枚举 private Integer status; // 参见 CommonStatusEnum 枚举

View File

@@ -0,0 +1,35 @@
package com.njcn.rdms.module.system.api.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户管理链路 Response DTO
*
* @author dklive
*/
@Schema(description = "RPC 服务 - 用户管理链路 Response DTO")
@Data
public class UserManagementRelationRespDTO {
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "管理者用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long managerUserId;
@Schema(description = "被管理用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long subordinateUserId;
@Schema(description = "生效开始时间")
private LocalDateTime effectiveFrom;
@Schema(description = "生效结束时间")
private LocalDateTime effectiveUntil;
@Schema(description = "备注")
private String remark;
}

View File

@@ -31,6 +31,7 @@ public interface ErrorCodeConstants {
ErrorCode MENU_ROUTE_PROPS_JSON_INVALID = new ErrorCode(1_002_001_009, "路由 props JSON 不合法"); ErrorCode MENU_ROUTE_PROPS_JSON_INVALID = new ErrorCode(1_002_001_009, "路由 props JSON 不合法");
ErrorCode MENU_ROUTE_IFRAME_URL_REQUIRED = new ErrorCode(1_002_001_010, "iframe 路由必须配置 props.url"); ErrorCode MENU_ROUTE_IFRAME_URL_REQUIRED = new ErrorCode(1_002_001_010, "iframe 路由必须配置 props.url");
ErrorCode MENU_ROUTE_NAME_DUPLICATE = new ErrorCode(1_002_001_011, "路由名重复,请检查菜单数据:{}"); ErrorCode MENU_ROUTE_NAME_DUPLICATE = new ErrorCode(1_002_001_011, "路由名重复,请检查菜单数据:{}");
ErrorCode MENU_SCOPE_NOT_MATCH = new ErrorCode(1_002_001_012, "菜单【{}】不属于当前作用域");
// ========== 角色模块 1-002-002-000 ========== // ========== 角色模块 1-002-002-000 ==========
ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在"); ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在");
@@ -39,6 +40,8 @@ public interface ErrorCodeConstants {
ErrorCode ROLE_CAN_NOT_DELETE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能删除类型为系统内置的角色"); ErrorCode ROLE_CAN_NOT_DELETE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能删除类型为系统内置的角色");
ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用"); ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用");
ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "标识【{}】不能使用"); ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "标识【{}】不能使用");
ErrorCode ROLE_SCOPE_NOT_MATCH = new ErrorCode(1_002_002_006, "角色【{}】不属于当前作用域");
ErrorCode ROLE_DISABLE_NOT_ALLOWED = new ErrorCode(1_002_005_006, "该角色还有用户在使用,不允许禁用");
// ========== 用户模块 1-002-003-000 ========== // ========== 用户模块 1-002-003-000 ==========
ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在"); ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在");
@@ -53,6 +56,11 @@ public interface ErrorCodeConstants {
ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭"); ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭");
ErrorCode USER_IS_RESIGNED = new ErrorCode(1_002_003_012, "名字为【{}】的用户已离职"); ErrorCode USER_IS_RESIGNED = new ErrorCode(1_002_003_012, "名字为【{}】的用户已离职");
// ========== 用户管理链路模块 1-002-003-100 ==========
ErrorCode USER_MANAGEMENT_RELATION_NOT_FOUND = new ErrorCode(1_002_003_100, "用户管理链路不存在");
ErrorCode USER_MANAGEMENT_RELATION_MANAGER_EXISTS = new ErrorCode(1_002_003_101, "该用户已有直属上级,不能重复添加");
ErrorCode USER_MANAGEMENT_RELATION_EXISTS = new ErrorCode(1_002_003_102, "该用户在管理链路中还在使用,不可删除!");
// ========== 部门模块 1-002-004-000 ========== // ========== 部门模块 1-002-004-000 ==========
ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门"); ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");
ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在"); ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在");
@@ -70,6 +78,7 @@ public interface ErrorCodeConstants {
ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1_002_005_002, "已经存在该名字的岗位"); ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1_002_005_002, "已经存在该名字的岗位");
ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1_002_005_003, "已经存在该标识的岗位"); ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1_002_005_003, "已经存在该标识的岗位");
ErrorCode POST_TYPE_INVALID = new ErrorCode(1_002_005_004, "岗位类型({})不合法"); ErrorCode POST_TYPE_INVALID = new ErrorCode(1_002_005_004, "岗位类型({})不合法");
ErrorCode POST_DISABLE_NOT_ALLOWED = new ErrorCode(1_002_005_005, "该岗位还有用户在使用,不允许禁用");
// ========== 字典类型 1-002-006-000 ========== // ========== 字典类型 1-002-006-000 ==========
ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1_002_006_001, "当前字典类型不存在"); ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1_002_006_001, "当前字典类型不存在");

View File

@@ -0,0 +1,20 @@
package com.njcn.rdms.module.system.enums.permission;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PermissionScopeTypeEnum {
GLOBAL("global"),
OBJECT("object");
/**
* 全局作用域的 objectType 固定为空字符串。
*/
public static final String GLOBAL_OBJECT_TYPE = "";
private final String scopeType;
}

View File

@@ -31,8 +31,6 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.njcn</groupId> <groupId>com.njcn</groupId>
<artifactId>rdms-spring-boot-starter-biz-ip</artifactId> <artifactId>rdms-spring-boot-starter-biz-ip</artifactId>
@@ -79,8 +77,6 @@
<artifactId>rdms-spring-boot-starter-websocket</artifactId> <artifactId>rdms-spring-boot-starter-websocket</artifactId>
</dependency> </dependency>
<!-- 消息队列相关 --> <!-- 消息队列相关 -->
<dependency> <dependency>
<groupId>com.njcn</groupId> <groupId>com.njcn</groupId>
@@ -125,6 +121,13 @@
<artifactId>s3</artifactId> <artifactId>s3</artifactId>
</dependency> </dependency>
<!-- 热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -136,6 +139,9 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version> <version>${spring.boot.version}</version>
<configuration>
<addResources>true</addResources> <!-- 开启热部署必须配置 -->
</configuration>
<executions> <executions>
<execution> <execution>
<goals> <goals>

View File

@@ -0,0 +1,48 @@
package com.njcn.rdms.module.system.api.user;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@RestController
@Validated
@Hidden
public class UserManagementRelationApiImpl implements UserManagementRelationApi {
@Resource
private UserManagementRelationService userManagementRelationService;
@Override
public CommonResult<List<UserManagementRelationRespDTO>> getRelationListByManagerUserId(Long managerUserId) {
List<UserManagementRelationDO> list = userManagementRelationService.getRelationListByManagerUserId(managerUserId);
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
}
@Override
public CommonResult<List<UserManagementRelationRespDTO>> getRelationListBySubordinateUserId(Long subordinateUserId) {
List<UserManagementRelationDO> list = userManagementRelationService.getRelationListBySubordinateUserId(subordinateUserId);
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
}
@Override
public CommonResult<List<UserManagementRelationRespDTO>> getRelationList(Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {
return success(Collections.emptyList());
}
List<UserManagementRelationDO> list = userManagementRelationService.getRelationList(ids);
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
}
}

View File

@@ -18,6 +18,7 @@ import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO;
import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO; import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO;
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO; import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
import com.njcn.rdms.module.system.enums.logger.LoginLogTypeEnum; import com.njcn.rdms.module.system.enums.logger.LoginLogTypeEnum;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.service.auth.AdminAuthService; import com.njcn.rdms.module.system.service.auth.AdminAuthService;
import com.njcn.rdms.module.system.service.permission.MenuService; import com.njcn.rdms.module.system.service.permission.MenuService;
import com.njcn.rdms.module.system.service.permission.PermissionService; import com.njcn.rdms.module.system.service.permission.PermissionService;
@@ -54,6 +55,9 @@ import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.
@Slf4j @Slf4j
public class AuthController { public class AuthController {
private static final String GLOBAL_SCOPE_TYPE = PermissionScopeTypeEnum.GLOBAL.getScopeType();
private static final String GLOBAL_OBJECT_TYPE = PermissionScopeTypeEnum.GLOBAL_OBJECT_TYPE;
@Resource @Resource
private AdminAuthService authService; private AdminAuthService authService;
@Resource @Resource
@@ -154,7 +158,7 @@ public class AuthController {
return Collections.emptyList(); return Collections.emptyList();
} }
List<RoleDO> roles = roleService.getRoleList(roleIds); List<RoleDO> roles = roleService.getRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus()));
return roles; return roles;
} }
@@ -164,8 +168,9 @@ public class AuthController {
return Collections.emptyList(); return Collections.emptyList();
} }
Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId)); Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId),
List<MenuDO> menuList = menuService.getMenuList(menuIds); GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
List<MenuDO> menuList = menuService.getMenuList(menuIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
return menuService.filterDisableMenus(menuList); return menuService.filterDisableMenus(menuList);
} }

View File

@@ -41,6 +41,9 @@ public class AuthPermissionInfoRespVO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能源码") @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能源码")
private String nickname; private String nickname;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.jpg") @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.jpg")
private String avatar; private String avatar;

View File

@@ -24,6 +24,10 @@ public class AuthRegisterReqVO extends CaptchaVerificationReqVO {
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符") @Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
private String nickname; private String nickname;
@Schema(description = "所属公司", example = "灿能")
@Size(max = 100, message = "所属公司长度不能超过 100 个字符")
private String company;
@Schema(description = "所属部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "所属部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "所属部门不能为空") @NotNull(message = "所属部门不能为空")
private Long deptId; private Long deptId;

View File

@@ -21,6 +21,9 @@ public class AuthUserInfoRespVO {
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin") @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
private String userName; private String userName;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "角色编码列表", example = "[\"SUPER_ADMIN\"]") @Schema(description = "角色编码列表", example = "[\"SUPER_ADMIN\"]")
private List<String> roles; private List<String> roles;

View File

@@ -12,7 +12,9 @@ import com.njcn.rdms.module.system.controller.admin.dict.vo.data.DictDataRespVO;
import com.njcn.rdms.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; import com.njcn.rdms.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO;
import com.njcn.rdms.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO; import com.njcn.rdms.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO;
import com.njcn.rdms.module.system.dal.dataobject.dict.DictDataDO; import com.njcn.rdms.module.system.dal.dataobject.dict.DictDataDO;
import com.njcn.rdms.module.system.dal.dataobject.dict.DictTypeDO;
import com.njcn.rdms.module.system.service.dict.DictDataService; import com.njcn.rdms.module.system.service.dict.DictDataService;
import com.njcn.rdms.module.system.service.dict.DictTypeService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -38,6 +40,9 @@ public class DictDataController {
@Resource @Resource
private DictDataService dictDataService; private DictDataService dictDataService;
@Resource
private DictTypeService dictTypeService;
@PostMapping("/create") @PostMapping("/create")
@Operation(summary = "新增字典数据") @Operation(summary = "新增字典数据")
@PreAuthorize("@ss.hasPermission('system:dict:create')") @PreAuthorize("@ss.hasPermission('system:dict:create')")
@@ -98,6 +103,16 @@ public class DictDataController {
return success(BeanUtils.toBean(dictData, DictDataRespVO.class)); return success(BeanUtils.toBean(dictData, DictDataRespVO.class));
} }
@GetMapping(value = "/code")
@Operation(summary = "/通过字典编码去查询字典数据详细")
@Parameter(name = "code", description = "编号", required = true, example = "system_user_company")
@PreAuthorize("@ss.hasPermission('system:dict:query')")
public CommonResult<List<DictDataRespVO>> getDictData(@RequestParam("code") String code) {
DictTypeDO dictType = dictTypeService.getDictType(code);
List<DictDataDO> dictDataList = dictDataService.getDictDataList(0, dictType.getType());
return success(BeanUtils.toBean(dictDataList, DictDataRespVO.class));
}
@GetMapping("/export-excel") @GetMapping("/export-excel")
@Operation(summary = "导出字典数据") @Operation(summary = "导出字典数据")
@PreAuthorize("@ss.hasPermission('system:dict:export')") @PreAuthorize("@ss.hasPermission('system:dict:export')")

View File

@@ -20,6 +20,9 @@ public class OAuth2UserInfoRespVO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能") @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能")
private String nickname; private String nickname;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "用户邮箱", example = "rdms@iocoder.cn") @Schema(description = "用户邮箱", example = "rdms@iocoder.cn")
private String email; private String email;

View File

@@ -1,13 +1,16 @@
package com.njcn.rdms.module.system.controller.admin.permission; package com.njcn.rdms.module.system.controller.admin.permission;
import cn.hutool.core.util.StrUtil;
import com.njcn.rdms.framework.common.enums.CommonStatusEnum; import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.validation.ValidationUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils; import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuRespVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuRespVO;
import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuSaveVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO;
import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO; import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.service.permission.MenuService; import com.njcn.rdms.module.system.service.permission.MenuService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@@ -36,6 +39,10 @@ import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Validated @Validated
public class MenuController { public class MenuController {
private static final String GLOBAL_SCOPE_TYPE = PermissionScopeTypeEnum.GLOBAL.getScopeType();
private static final String OBJECT_SCOPE_TYPE = PermissionScopeTypeEnum.OBJECT.getScopeType();
private static final String GLOBAL_OBJECT_TYPE = PermissionScopeTypeEnum.GLOBAL_OBJECT_TYPE;
@Resource @Resource
private MenuService menuService; private MenuService menuService;
@@ -43,7 +50,7 @@ public class MenuController {
@Operation(summary = "创建菜单") @Operation(summary = "创建菜单")
@PreAuthorize("@ss.hasPermission('system:menu:create')") @PreAuthorize("@ss.hasPermission('system:menu:create')")
public CommonResult<Long> createMenu(@Valid @RequestBody MenuSaveVO createReqVO) { public CommonResult<Long> createMenu(@Valid @RequestBody MenuSaveVO createReqVO) {
Long menuId = menuService.createMenu(createReqVO); Long menuId = menuService.createMenu(normalizeScopeReqVO(createReqVO));
return success(menuId); return success(menuId);
} }
@@ -77,15 +84,20 @@ public class MenuController {
@Operation(summary = "获取菜单列表", description = "用于【菜单管理】界面") @Operation(summary = "获取菜单列表", description = "用于【菜单管理】界面")
@PreAuthorize("@ss.hasPermission('system:menu:query')") @PreAuthorize("@ss.hasPermission('system:menu:query')")
public CommonResult<List<MenuRespVO>> getMenuList(MenuListReqVO reqVO) { public CommonResult<List<MenuRespVO>> getMenuList(MenuListReqVO reqVO) {
List<MenuDO> list = menuService.getMenuList(reqVO); MenuListReqVO effectiveReqVO = normalizeScopeReqVO(reqVO);
List<MenuDO> list = menuService.getMenuList(effectiveReqVO,
effectiveReqVO.getScopeType(), effectiveReqVO.getObjectType());
list.sort(Comparator.comparing(MenuDO::getSort)); list.sort(Comparator.comparing(MenuDO::getSort));
return success(BeanUtils.toBean(list, MenuRespVO.class)); return success(BeanUtils.toBean(list, MenuRespVO.class));
} }
@GetMapping("/simple-list") @GetMapping("/simple-list")
@Operation(summary = "获取菜单精简信息列表", description = "只包含已启用的菜单,用于【角色分配菜单】功能的选项") @Operation(summary = "获取菜单精简信息列表", description = "只包含已启用的菜单,用于【角色分配菜单】功能的选项")
public CommonResult<List<MenuSimpleRespVO>> getSimpleMenuList() { public CommonResult<List<MenuSimpleRespVO>> getSimpleMenuList(MenuListReqVO reqVO) {
List<MenuDO> list = menuService.getMenuList(new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus())); MenuListReqVO effectiveReqVO = normalizeScopeReqVO(reqVO);
effectiveReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
List<MenuDO> list = menuService.getMenuList(effectiveReqVO,
effectiveReqVO.getScopeType(), effectiveReqVO.getObjectType());
list = menuService.filterDisableMenus(list); list = menuService.filterDisableMenus(list);
list.sort(Comparator.comparing(MenuDO::getSort)); list.sort(Comparator.comparing(MenuDO::getSort));
return success(BeanUtils.toBean(list, MenuSimpleRespVO.class)); return success(BeanUtils.toBean(list, MenuSimpleRespVO.class));
@@ -99,4 +111,30 @@ public class MenuController {
return success(BeanUtils.toBean(menu, MenuRespVO.class)); return success(BeanUtils.toBean(menu, MenuRespVO.class));
} }
private MenuListReqVO normalizeScopeReqVO(MenuListReqVO reqVO) {
MenuListReqVO effectiveReqVO = reqVO == null ? new MenuListReqVO() : reqVO;
String scopeType = normalizeScopeType(effectiveReqVO.getScopeType());
effectiveReqVO.setScopeType(scopeType);
effectiveReqVO.setObjectType(normalizeObjectType(scopeType, effectiveReqVO.getObjectType()));
ValidationUtils.validate(effectiveReqVO);
return effectiveReqVO;
}
private MenuSaveVO normalizeScopeReqVO(MenuSaveVO reqVO) {
MenuSaveVO effectiveReqVO = reqVO == null ? new MenuSaveVO() : reqVO;
String scopeType = normalizeScopeType(effectiveReqVO.getScopeType());
effectiveReqVO.setScopeType(scopeType);
effectiveReqVO.setObjectType(normalizeObjectType(scopeType, effectiveReqVO.getObjectType()));
ValidationUtils.validate(effectiveReqVO);
return effectiveReqVO;
}
private String normalizeScopeType(String scopeType) {
return StrUtil.blankToDefault(StrUtil.trim(scopeType), GLOBAL_SCOPE_TYPE);
}
private String normalizeObjectType(String scopeType, String objectType) {
return OBJECT_SCOPE_TYPE.equals(scopeType) ? StrUtil.trim(objectType) : GLOBAL_OBJECT_TYPE;
}
} }

View File

@@ -1,16 +1,19 @@
package com.njcn.rdms.module.system.controller.admin.permission; package com.njcn.rdms.module.system.controller.admin.permission;
import cn.hutool.core.util.StrUtil;
import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog; import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog;
import com.njcn.rdms.framework.common.enums.CommonStatusEnum; import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageParam; import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils; import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.common.util.validation.ValidationUtils;
import com.njcn.rdms.framework.excel.core.util.ExcelUtils; import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RolePageReqVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RolePageReqVO;
import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RoleRespVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RoleRespVO;
import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO; import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.service.permission.RoleService; import com.njcn.rdms.module.system.service.permission.RoleService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@@ -36,6 +39,10 @@ import static java.util.Collections.singleton;
@Validated @Validated
public class RoleController { public class RoleController {
private static final String GLOBAL_SCOPE_TYPE = PermissionScopeTypeEnum.GLOBAL.getScopeType();
private static final String OBJECT_SCOPE_TYPE = PermissionScopeTypeEnum.OBJECT.getScopeType();
private static final String GLOBAL_OBJECT_TYPE = PermissionScopeTypeEnum.GLOBAL_OBJECT_TYPE;
@Resource @Resource
private RoleService roleService; private RoleService roleService;
@@ -43,7 +50,7 @@ public class RoleController {
@Operation(summary = "创建角色") @Operation(summary = "创建角色")
@PreAuthorize("@ss.hasPermission('system:role:create')") @PreAuthorize("@ss.hasPermission('system:role:create')")
public CommonResult<Long> createRole(@Valid @RequestBody RoleSaveReqVO createReqVO) { public CommonResult<Long> createRole(@Valid @RequestBody RoleSaveReqVO createReqVO) {
return success(roleService.createRole(createReqVO, null)); return success(roleService.createRole(normalizeScopeReqVO(createReqVO), null));
} }
@PutMapping("/update") @PutMapping("/update")
@@ -84,15 +91,19 @@ public class RoleController {
@Operation(summary = "获得角色分页") @Operation(summary = "获得角色分页")
@PreAuthorize("@ss.hasPermission('system:role:query')") @PreAuthorize("@ss.hasPermission('system:role:query')")
public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) { public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); RolePageReqVO effectiveReqVO = normalizeScopeReqVO(pageReqVO);
PageResult<RoleDO> pageResult = roleService.getRolePage(pageReqVO); effectiveReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
PageResult<RoleDO> pageResult = roleService.getRolePage(effectiveReqVO,
effectiveReqVO.getScopeType(), effectiveReqVO.getObjectType());
return success(BeanUtils.toBean(pageResult, RoleRespVO.class)); return success(BeanUtils.toBean(pageResult, RoleRespVO.class));
} }
@GetMapping("/simple-list") @GetMapping("/simple-list")
@Operation(summary = "获取角色精简信息列表", description = "只包含被开启的角色,主要用于前端的下拉选项") @Operation(summary = "获取角色精简信息列表", description = "只包含被开启的角色,主要用于前端的下拉选项")
public CommonResult<List<RoleRespVO>> getSimpleRoleList() { public CommonResult<List<RoleRespVO>> getSimpleRoleList(RolePageReqVO reqVO) {
List<RoleDO> list = roleService.getRoleListByStatus(singleton(CommonStatusEnum.ENABLE.getStatus())); RolePageReqVO effectiveReqVO = normalizeScopeReqVO(reqVO);
List<RoleDO> list = roleService.getRoleListByStatus(singleton(CommonStatusEnum.ENABLE.getStatus()),
effectiveReqVO.getScopeType(), effectiveReqVO.getObjectType());
list.sort(Comparator.comparing(RoleDO::getSort)); list.sort(Comparator.comparing(RoleDO::getSort));
return success(BeanUtils.toBean(list, RoleRespVO.class)); return success(BeanUtils.toBean(list, RoleRespVO.class));
} }
@@ -109,4 +120,30 @@ public class RoleController {
BeanUtils.toBean(list, RoleRespVO.class)); BeanUtils.toBean(list, RoleRespVO.class));
} }
private RolePageReqVO normalizeScopeReqVO(RolePageReqVO reqVO) {
RolePageReqVO effectiveReqVO = reqVO == null ? new RolePageReqVO() : reqVO;
String scopeType = normalizeScopeType(effectiveReqVO.getScopeType());
effectiveReqVO.setScopeType(scopeType);
effectiveReqVO.setObjectType(normalizeObjectType(scopeType, effectiveReqVO.getObjectType()));
ValidationUtils.validate(effectiveReqVO);
return effectiveReqVO;
}
private RoleSaveReqVO normalizeScopeReqVO(RoleSaveReqVO reqVO) {
RoleSaveReqVO effectiveReqVO = reqVO == null ? new RoleSaveReqVO() : reqVO;
String scopeType = normalizeScopeType(effectiveReqVO.getScopeType());
effectiveReqVO.setScopeType(scopeType);
effectiveReqVO.setObjectType(normalizeObjectType(scopeType, effectiveReqVO.getObjectType()));
ValidationUtils.validate(effectiveReqVO);
return effectiveReqVO;
}
private String normalizeScopeType(String scopeType) {
return StrUtil.blankToDefault(StrUtil.trim(scopeType), GLOBAL_SCOPE_TYPE);
}
private String normalizeObjectType(String scopeType, String objectType) {
return OBJECT_SCOPE_TYPE.equals(scopeType) ? StrUtil.trim(objectType) : GLOBAL_OBJECT_TYPE;
}
} }

View File

@@ -1,6 +1,9 @@
package com.njcn.rdms.module.system.controller.admin.permission.vo.menu; package com.njcn.rdms.module.system.controller.admin.permission.vo.menu;
import cn.hutool.core.util.StrUtil;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import lombok.Data; import lombok.Data;
@Schema(description = "管理后台 - 菜单列表 Request VO") @Schema(description = "管理后台 - 菜单列表 Request VO")
@@ -13,4 +16,23 @@ public class MenuListReqVO {
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
private Integer status; private Integer status;
@Schema(description = "作用域类型global 或 object", example = "global")
private String scopeType;
@Schema(description = "对象类型,当 scopeType=object 时必填", example = "product")
private String objectType;
@AssertTrue(message = "scopeType 只能是 global 或 object")
public boolean isScopeTypeValid() {
return StrUtil.isBlank(scopeType)
|| PermissionScopeTypeEnum.GLOBAL.getScopeType().equals(scopeType)
|| PermissionScopeTypeEnum.OBJECT.getScopeType().equals(scopeType);
}
@AssertTrue(message = "scopeType=object 时 objectType 不能为空")
public boolean isObjectTypeValid() {
return !PermissionScopeTypeEnum.OBJECT.getScopeType().equals(scopeType)
|| StrUtil.isNotBlank(objectType);
}
} }

View File

@@ -1,6 +1,9 @@
package com.njcn.rdms.module.system.controller.admin.permission.vo.menu; package com.njcn.rdms.module.system.controller.admin.permission.vo.menu;
import cn.hutool.core.util.StrUtil;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
@@ -68,4 +71,23 @@ public class MenuSaveVO {
@Schema(description = "是否总是显示", example = "false") @Schema(description = "是否总是显示", example = "false")
private Boolean alwaysShow; private Boolean alwaysShow;
@Schema(description = "作用域类型global 或 object", example = "global")
private String scopeType;
@Schema(description = "对象类型,当 scopeType=object 时必填", example = "product")
private String objectType;
@AssertTrue(message = "scopeType 只能是 global 或 object")
public boolean isScopeTypeValid() {
return StrUtil.isBlank(scopeType)
|| PermissionScopeTypeEnum.GLOBAL.getScopeType().equals(scopeType)
|| PermissionScopeTypeEnum.OBJECT.getScopeType().equals(scopeType);
}
@AssertTrue(message = "scopeType=object 时 objectType 不能为空")
public boolean isObjectTypeValid() {
return !PermissionScopeTypeEnum.OBJECT.getScopeType().equals(scopeType)
|| StrUtil.isNotBlank(objectType);
}
} }

View File

@@ -1,7 +1,10 @@
package com.njcn.rdms.module.system.controller.admin.permission.vo.role; package com.njcn.rdms.module.system.controller.admin.permission.vo.role;
import cn.hutool.core.util.StrUtil;
import com.njcn.rdms.framework.common.pojo.PageParam; import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
@@ -28,4 +31,23 @@ public class RolePageReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime; private LocalDateTime[] createTime;
@Schema(description = "作用域类型global 或 object", example = "global")
private String scopeType;
@Schema(description = "对象类型,当 scopeType=object 时必填", example = "product")
private String objectType;
@AssertTrue(message = "scopeType 只能是 global 或 object")
public boolean isScopeTypeValid() {
return StrUtil.isBlank(scopeType)
|| PermissionScopeTypeEnum.GLOBAL.getScopeType().equals(scopeType)
|| PermissionScopeTypeEnum.OBJECT.getScopeType().equals(scopeType);
}
@AssertTrue(message = "scopeType=object 时 objectType 不能为空")
public boolean isObjectTypeValid() {
return !PermissionScopeTypeEnum.OBJECT.getScopeType().equals(scopeType)
|| StrUtil.isNotBlank(objectType);
}
} }

View File

@@ -1,9 +1,12 @@
package com.njcn.rdms.module.system.controller.admin.permission.vo.role; package com.njcn.rdms.module.system.controller.admin.permission.vo.role;
import cn.hutool.core.util.StrUtil;
import com.njcn.rdms.framework.common.enums.CommonStatusEnum; import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.common.validation.InEnum; import com.njcn.rdms.framework.common.validation.InEnum;
import com.mzt.logapi.starter.annotation.DiffLogField; import com.mzt.logapi.starter.annotation.DiffLogField;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
@@ -44,4 +47,23 @@ public class RoleSaveReqVO {
@DiffLogField(name = "备注") @DiffLogField(name = "备注")
private String remark; private String remark;
@Schema(description = "作用域类型global 或 object", example = "global")
private String scopeType;
@Schema(description = "对象类型,当 scopeType=object 时必填", example = "product")
private String objectType;
@AssertTrue(message = "scopeType 只能是 global 或 object")
public boolean isScopeTypeValid() {
return StrUtil.isBlank(scopeType)
|| PermissionScopeTypeEnum.GLOBAL.getScopeType().equals(scopeType)
|| PermissionScopeTypeEnum.OBJECT.getScopeType().equals(scopeType);
}
@AssertTrue(message = "scopeType=object 时 objectType 不能为空")
public boolean isObjectTypeValid() {
return !PermissionScopeTypeEnum.OBJECT.getScopeType().equals(scopeType)
|| StrUtil.isNotBlank(objectType);
}
} }

View File

@@ -141,6 +141,15 @@ public class UserController {
return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap)); return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap));
} }
@GetMapping("/list-by-dept-id")
@Operation(summary = "根据部门ID获取该部门和下属部门的用户精简信息列表不分页")
@Parameter(name = "deptId", description = "部门ID", required = true)
public CommonResult<List<UserSimpleRespVO>> getUserListByDeptId(@RequestParam("deptId") Long deptId) {
List<AdminUserDO> list = userService.getAllUserByDeptId(deptId);
Map<Long, DeptDO> deptMap = deptService.getDeptMap(convertList(list, AdminUserDO::getDeptId));
return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap));
}
@GetMapping("/get") @GetMapping("/get")
@Operation(summary = "获得用户详情") @Operation(summary = "获得用户详情")
@Parameter(name = "id", description = "编号", required = true, example = "1024") @Parameter(name = "id", description = "编号", required = true, example = "1024")

View File

@@ -0,0 +1,191 @@
package com.njcn.rdms.module.system.controller.admin.user;
import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationQueryReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationRespVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationSaveReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationTreeRespVO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
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.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
/**
* 用户管理链路 Controller
*
* 提供用户管理链路的管理接口,包括:
* - 创建、更新、删除用户管理链路
* - 查询用户管理链路列表和详情
* - 获取用户管理链路树形结构
* - 导出用户管理链路数据
*
* @author dklive
*/
@Tag(name = "管理后台 - 用户管理链路")
@RestController
@RequestMapping("/system/user-management-relation")
@Validated
public class UserManagementRelationController {
@Resource
private UserManagementRelationService userManagementRelationService;
/**
* 创建用户管理链路
*
* 权限要求system:user-management-relation:create
*
* @param createReqVO 创建请求VO
* @return 关系记录主键ID
*/
@PostMapping("/create")
@Operation(summary = "创建用户管理链路")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:create')")
public CommonResult<Long> createUserManagementRelation(@Valid @RequestBody UserManagementRelationSaveReqVO createReqVO) {
Long id = userManagementRelationService.createRelation(createReqVO);
return success(id);
}
/**
* 修改用户管理链路
*
* 权限要求system:user-management-relation:update
*
* @param updateReqVO 更新请求VO
* @return 操作结果
*/
@PutMapping("/update")
@Operation(summary = "修改用户管理链路")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:update')")
public CommonResult<Boolean> updateUserManagementRelation(@Valid @RequestBody UserManagementRelationSaveReqVO updateReqVO) {
userManagementRelationService.updateRelation(updateReqVO);
return success(true);
}
/**
* 删除用户管理链路
*
* 根据主键ID删除单条用户管理链路记录
* 权限要求system:user-management-relation:delete
*
* @param id 关系记录主键ID
* @return 操作结果
*/
@DeleteMapping("/delete")
@Operation(summary = "删除用户管理链路")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:delete')")
public CommonResult<Boolean> deleteUserManagementRelation(@RequestParam("id") Long id) {
userManagementRelationService.deleteRelation(id);
return success(true);
}
/**
* 批量删除用户管理链路
*
* 根据主键ID列表批量删除用户管理链路记录
* 权限要求system:user-management-relation:delete
*
* @param ids 关系记录主键ID列表
* @return 操作结果
*/
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除用户管理链路")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:delete')")
public CommonResult<Boolean> deleteUserManagementRelationList(@RequestParam("ids") List<Long> ids) {
userManagementRelationService.deleteRelationList(ids);
return success(true);
}
/**
* 获得用户管理链路信息
*
* 根据主键ID查询单条用户管理链路记录
* 权限要求system:user-management-relation:query
*
* @param id 关系记录主键ID
* @return 用户管理链路详情
*/
@GetMapping(value = "/get")
@Operation(summary = "获得用户管理链路信息")
@Parameter(name = "id", description = "关系编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
public CommonResult<UserManagementRelationRespVO> getUserManagementRelation(@RequestParam("id") Long id) {
UserManagementRelationDO relation = userManagementRelationService.getRelation(id);
return success(BeanUtils.toBean(relation, UserManagementRelationRespVO.class));
}
/**
* 获取用户管理链路列表
*
* 根据查询条件查询用户管理链路记录列表
* 权限要求system:user-management-relation:query
*
* @param reqVO 查询条件VO
* @return 用户管理链路列表
*/
@GetMapping("/query")
@Operation(summary = "获取用户管理链路列表")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
public CommonResult<List<UserManagementRelationTreeRespVO>> getUserManagementRelationQuery(@Validated UserManagementRelationQueryReqVO reqVO) {
List<UserManagementRelationTreeRespVO> list = userManagementRelationService.getRelationQuery(reqVO);
return success(list);
}
/**
* 获取用户管理链路树形结构
*
* 构建用户上下级关系的树形结构,用于前端树形控件展示
* 树形结构特点:
* - 根节点:最高领导,没有上级
* - 中间节点:有上级也有下级
* - 叶子节点:基层员工,没有下级
*
* 权限要求system:user-management-relation:query
*
* @return 用户管理链路树形列表
*/
@GetMapping("/tree")
@Operation(summary = "获取用户管理链路树形结构", description = "用于前端树形控件展示,包含用户的上下级层级关系")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
public CommonResult<List<UserManagementRelationTreeRespVO>> getUserManagementRelationTree(@Validated UserManagementRelationQueryReqVO reqVO) {
return success(userManagementRelationService.getRelationTree(reqVO));
}
/**
* 导出用户管理链路 Excel
*
* 根据查询条件导出用户管理链路数据到Excel文件
* 权限要求system:user-management-relation:export
*
* @param response HTTP响应对象
* @param reqVO 查询条件VO
* @throws IOException IO异常
*/
@GetMapping("/export-excel")
@Operation(summary = "导出用户管理链路 Excel")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:export')")
@ApiAccessLog(operateType = EXPORT)
public void export(HttpServletResponse response, @Validated UserManagementRelationQueryReqVO reqVO) throws IOException {
List<UserManagementRelationTreeRespVO> list = userManagementRelationService.getRelationQuery(reqVO);
ExcelUtils.write(response, "用户管理链路数据.xls", "用户管理链路列表", UserManagementRelationRespVO.class,
BeanUtils.toBean(list, UserManagementRelationRespVO.class));
}
}

View File

@@ -22,6 +22,9 @@ public class UserProfileRespVO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen") @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen")
private String nickname; private String nickname;
@Schema(description = "所属公司", example = "灿能")
private String company;
@Schema(description = "用户邮箱", example = "rdms@iocoder.cn") @Schema(description = "用户邮箱", example = "rdms@iocoder.cn")
private String email; private String email;

View File

@@ -24,6 +24,9 @@ public class UserImportExcelVO {
@ExcelProperty("用户名称") @ExcelProperty("用户名称")
private String nickname; private String nickname;
@ExcelProperty("所属公司")
private String company;
@ExcelProperty("部门编号") @ExcelProperty("部门编号")
private Long deptId; private Long deptId;

View File

@@ -25,6 +25,9 @@ public class UserPageReqVO extends PageParam {
@Schema(description = "手机号码,模糊匹配", example = "rdms") @Schema(description = "手机号码,模糊匹配", example = "rdms")
private String mobile; private String mobile;
@Schema(description = "所属公司,模糊匹配", example = "灿能")
private String company;
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
private Integer status; private Integer status;

View File

@@ -30,6 +30,10 @@ public class UserRespVO {
@Schema(description = "备注", example = "我是一个用户") @Schema(description = "备注", example = "我是一个用户")
private String remark; private String remark;
@Schema(description = "所属公司", example = "灿能")
@ExcelProperty("所属公司")
private String company;
@Schema(description = "部门编号", example = "1") @Schema(description = "部门编号", example = "1")
private Long deptId; private Long deptId;

View File

@@ -33,7 +33,8 @@ public class UserSaveReqVO {
@DiffLogField(name = "用户账号") @DiffLogField(name = "用户账号")
private String username; private String username;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "awen") @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "戴坤")
@NotBlank(message = "用户昵称不能为空")
@Size(max = 30, message = "用户昵称长度不能超过 30 个字符") @Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
@DiffLogField(name = "用户昵称") @DiffLogField(name = "用户昵称")
private String nickname; private String nickname;
@@ -42,6 +43,11 @@ public class UserSaveReqVO {
@DiffLogField(name = "备注") @DiffLogField(name = "备注")
private String remark; private String remark;
@Schema(description = "所属公司", example = "灿能")
@Size(max = 100, message = "所属公司长度不能超过 100 个字符")
@DiffLogField(name = "所属公司")
private String company;
@Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "部门不能为空") @NotNull(message = "部门不能为空")
@DiffLogField(name = "部门", function = DeptParseFunction.NAME) @DiffLogField(name = "部门", function = DeptParseFunction.NAME)

View File

@@ -0,0 +1,21 @@
package com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 用户管理链路列表 Request VO")
@Data
public class UserManagementRelationQueryReqVO {
@Schema(description = "管理者用户ID", example = "1")
private Long managerUserId;
@Schema(description = "被管理用户ID", example = "2")
private Long subordinateUserId;
@Schema(description = "访问是否来自user/index组件", example = "true/false")
private Boolean fromUserIndex;
@Schema(description = "所选中的部门id", example = "100灿能电力的部门id")
private Long deptId;
}

View File

@@ -0,0 +1,42 @@
package com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 用户管理链路信息 Response VO")
@Data
@ExcelIgnoreUnannotated
public class UserManagementRelationRespVO {
@Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("主键ID")
private Long id;
@Schema(description = "管理者用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty("管理者用户ID")
private Long managerUserId;
@Schema(description = "被管理用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("被管理用户ID")
private Long subordinateUserId;
@Schema(description = "生效开始时间")
@ExcelProperty("生效开始时间")
private LocalDateTime effectiveFrom;
@Schema(description = "生效结束时间")
@ExcelProperty("生效结束时间")
private LocalDateTime effectiveUntil;
@Schema(description = "备注", example = "直属上级关系")
@ExcelProperty("备注")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,33 @@
package com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 用户管理链路创建/修改 Request VO")
@Data
public class UserManagementRelationSaveReqVO {
@Schema(description = "主键ID", example = "1024")
private Long id;
@Schema(description = "管理者用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "管理者用户ID不能为空")
private Long managerUserId;
@Schema(description = "被管理用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "被管理用户ID不能为空")
private Long subordinateUserId;
@Schema(description = "生效开始时间")
private LocalDateTime effectiveFrom;
@Schema(description = "生效结束时间")
private LocalDateTime effectiveUntil;
@Schema(description = "备注", example = "直属上级关系")
private String remark;
}

View File

@@ -0,0 +1,60 @@
package com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 用户管理链路树形 Response VO
*
* 用于前端树形控件展示用户的上下级层级关系
* 包含关系记录的主键ID便于前端执行删除和更新操作
*
* @author hongawen
*/
@Schema(description = "管理后台 - 用户管理链路树形 Response VO")
@Data
public class UserManagementRelationTreeRespVO {
/**
* 关系记录主键ID
* 用于前端执行删除和更新操作
*/
@Schema(description = "关系记录主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
/**
* 用户ID
*/
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long userId;
/**
* 用户昵称
*/
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
private String userNickname;
/**
* 上级用户ID
* 最高领导此字段为null
*/
@Schema(description = "上级用户ID", example = "1")
private Long managerUserId;
/**
* 上级用户昵称
* 最高领导此字段为null
*/
@Schema(description = "上级用户昵称", example = "李四")
private String managerNickname;
/**
* 下级用户列表
* 基层员工此字段为空列表
*/
@Schema(description = "下级用户列表")
private List<UserManagementRelationTreeRespVO> children;
}

View File

@@ -57,6 +57,7 @@ public interface AuthConvert {
return AuthUserInfoRespVO.builder() return AuthUserInfoRespVO.builder()
.userId(String.valueOf(user.getId())) .userId(String.valueOf(user.getId()))
.userName(user.getUsername()) .userName(user.getUsername())
.company(user.getCompany())
.roles(sortDistinctStrings(convertList(roleList, RoleDO::getCode))) .roles(sortDistinctStrings(convertList(roleList, RoleDO::getCode)))
.buttons(sortDistinctStrings(convertList(menuList, MenuDO::getPermission, .buttons(sortDistinctStrings(convertList(menuList, MenuDO::getPermission,
menu -> StrUtil.isNotBlank(menu.getPermission())))) menu -> StrUtil.isNotBlank(menu.getPermission()))))

View File

@@ -3,6 +3,7 @@ package com.njcn.rdms.module.system.dal.dataobject.permission;
import com.njcn.rdms.framework.common.enums.CommonStatusEnum; import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum; import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
@@ -45,6 +46,18 @@ public class MenuDO extends BaseDO {
* - 对于前端,配合前端标签,配置按钮是否展示,避免用户没有该权限时,结果可以看到该操作。 * - 对于前端,配合前端标签,配置按钮是否展示,避免用户没有该权限时,结果可以看到该操作。
*/ */
private String permission; private String permission;
/**
* 作用域类型
*
* 枚举 {@link PermissionScopeTypeEnum}
*/
private String scopeType;
/**
* 对象类型
*
* 全局资源固定为空字符串
*/
private String objectType;
/** /**
* 菜单类型 * 菜单类型
* *

View File

@@ -2,6 +2,7 @@ package com.njcn.rdms.module.system.dal.dataobject.permission;
import com.njcn.rdms.framework.common.enums.CommonStatusEnum; import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.enums.permission.RoleTypeEnum; import com.njcn.rdms.module.system.enums.permission.RoleTypeEnum;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
@@ -33,6 +34,18 @@ public class RoleDO extends BaseDO {
* 枚举 * 枚举
*/ */
private String code; private String code;
/**
* 作用域类型
*
* 枚举 {@link PermissionScopeTypeEnum}
*/
private String scopeType;
/**
* 对象类型
*
* 全局角色固定为空字符串
*/
private String objectType;
/** /**
* 角色排序 * 角色排序
*/ */

View File

@@ -57,6 +57,11 @@ public class AdminUserDO extends BaseDO {
*/ */
private String remark; private String remark;
/**
* 所属公司
*/
private String company;
/** /**
* 部门 ID * 部门 ID
*/ */

View File

@@ -0,0 +1,77 @@
package com.njcn.rdms.module.system.dal.dataobject.user;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 用户管理链路表 DO
*
* 用于存储用户之间的直属上下级管理关系
* 每条记录代表一个管理者与被管理者之间的关系
*
* 表名system_user_management_relation
*
* 业务场景:
* - 组织架构中的直属上下级关系管理
* - 支持关系的生效时间范围设置
* - 一个用户可以有多个上级,但只有一个直属上级
* - 一个用户可以有多个下属
*
* @author dklive
*/
@TableName("system_user_management_relation")
@Data
@EqualsAndHashCode(callSuper = true)
public class UserManagementRelationDO extends BaseDO {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 管理者用户ID
*
* 表示上级用户的ID即管理者的用户ID
* 对应 system_users 表的 id 字段
*/
private Long managerUserId;
/**
* 被管理用户ID
*
* 表示下级用户的ID即被管理者的用户ID
* 对应 system_users 表的 id 字段
*/
private Long subordinateUserId;
/**
* 生效开始时间
*
* 关系开始生效的时间
* 为空表示立即长期生效
*/
private LocalDateTime effectiveFrom;
/**
* 生效结束时间
*
* 关系失效的时间
* 为空表示长期有效
*/
private LocalDateTime effectiveUntil;
/**
* 备注
*
* 用于记录关系的额外说明信息
*/
private String remark;
}

View File

@@ -5,6 +5,7 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO; import com.njcn.rdms.module.system.dal.dataobject.permission.MenuDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.springframework.lang.Nullable;
import java.util.List; import java.util.List;
@@ -12,7 +13,15 @@ import java.util.List;
public interface MenuMapper extends BaseMapperX<MenuDO> { public interface MenuMapper extends BaseMapperX<MenuDO> {
default MenuDO selectByParentIdAndName(Long parentId, String name) { default MenuDO selectByParentIdAndName(Long parentId, String name) {
return selectOne(MenuDO::getParentId, parentId, MenuDO::getName, name); return selectByParentIdAndName(parentId, name, null, null);
}
default MenuDO selectByParentIdAndName(Long parentId, String name, @Nullable String scopeType, @Nullable String objectType) {
return selectOne(new LambdaQueryWrapperX<MenuDO>()
.eq(MenuDO::getParentId, parentId)
.eq(MenuDO::getName, name)
.eq(scopeType != null, MenuDO::getScopeType, scopeType)
.eq(objectType != null, MenuDO::getObjectType, objectType));
} }
default Long selectCountByParentId(Long parentId) { default Long selectCountByParentId(Long parentId) {
@@ -20,17 +29,37 @@ public interface MenuMapper extends BaseMapperX<MenuDO> {
} }
default List<MenuDO> selectList(MenuListReqVO reqVO) { default List<MenuDO> selectList(MenuListReqVO reqVO) {
return selectList(reqVO, null, null);
}
default List<MenuDO> selectList(MenuListReqVO reqVO, @Nullable String scopeType, @Nullable String objectType) {
return selectList(new LambdaQueryWrapperX<MenuDO>() return selectList(new LambdaQueryWrapperX<MenuDO>()
.likeIfPresent(MenuDO::getName, reqVO.getName()) .likeIfPresent(MenuDO::getName, reqVO.getName())
.eqIfPresent(MenuDO::getStatus, reqVO.getStatus())); .eqIfPresent(MenuDO::getStatus, reqVO.getStatus())
.eq(scopeType != null, MenuDO::getScopeType, scopeType)
.eq(objectType != null, MenuDO::getObjectType, objectType));
} }
default List<MenuDO> selectListByPermission(String permission) { default List<MenuDO> selectListByPermission(String permission) {
return selectList(MenuDO::getPermission, permission); return selectListByPermission(permission, null, null);
}
default List<MenuDO> selectListByPermission(String permission, @Nullable String scopeType, @Nullable String objectType) {
return selectList(new LambdaQueryWrapperX<MenuDO>()
.eq(MenuDO::getPermission, permission)
.eq(scopeType != null, MenuDO::getScopeType, scopeType)
.eq(objectType != null, MenuDO::getObjectType, objectType));
} }
default MenuDO selectByComponentName(String componentName) { default MenuDO selectByComponentName(String componentName) {
return selectOne(MenuDO::getComponentName, componentName); return selectByComponentName(componentName, null, null);
}
default MenuDO selectByComponentName(String componentName, @Nullable String scopeType, @Nullable String objectType) {
return selectOne(new LambdaQueryWrapperX<MenuDO>()
.eq(MenuDO::getComponentName, componentName)
.eq(scopeType != null, MenuDO::getScopeType, scopeType)
.eq(objectType != null, MenuDO::getObjectType, objectType));
} }
} }

View File

@@ -17,6 +17,10 @@ import java.util.List;
public interface RoleMapper extends BaseMapperX<RoleDO> { public interface RoleMapper extends BaseMapperX<RoleDO> {
default PageResult<RoleDO> selectPage(RolePageReqVO reqVO) { default PageResult<RoleDO> selectPage(RolePageReqVO reqVO) {
return selectPageByScope(reqVO, null, null);
}
default PageResult<RoleDO> selectPageByScope(RolePageReqVO reqVO, @Nullable String scopeType, @Nullable String objectType) {
LambdaQueryWrapperX<RoleDO> queryWrapper = new LambdaQueryWrapperX<>(); LambdaQueryWrapperX<RoleDO> queryWrapper = new LambdaQueryWrapperX<>();
boolean hasName = StringUtils.hasText(reqVO.getName()); boolean hasName = StringUtils.hasText(reqVO.getName());
boolean hasCode = StringUtils.hasText(reqVO.getCode()); boolean hasCode = StringUtils.hasText(reqVO.getCode());
@@ -29,21 +33,47 @@ public interface RoleMapper extends BaseMapperX<RoleDO> {
.likeIfPresent(RoleDO::getCode, reqVO.getCode()); .likeIfPresent(RoleDO::getCode, reqVO.getCode());
} }
queryWrapper.eqIfPresent(RoleDO::getStatus, reqVO.getStatus()) queryWrapper.eqIfPresent(RoleDO::getStatus, reqVO.getStatus())
.eq(scopeType != null, RoleDO::getScopeType, scopeType)
.eq(objectType != null, RoleDO::getObjectType, objectType)
.betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime()) .betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime())
.orderByAsc(RoleDO::getSort); .orderByAsc(RoleDO::getSort);
return selectPage(reqVO, queryWrapper); return BaseMapperX.super.selectPage(reqVO, queryWrapper);
} }
default RoleDO selectByName(String name) { default RoleDO selectByName(String name) {
return selectOne(RoleDO::getName, name); return selectByName(name, null, null);
}
default RoleDO selectByName(String name, @Nullable String scopeType, @Nullable String objectType) {
return selectOne(new LambdaQueryWrapperX<RoleDO>()
.eq(RoleDO::getName, name)
.eq(scopeType != null, RoleDO::getScopeType, scopeType)
.eq(objectType != null, RoleDO::getObjectType, objectType));
} }
default RoleDO selectByCode(String code) { default RoleDO selectByCode(String code) {
return selectOne(RoleDO::getCode, code); return selectByCode(code, null, null);
}
default RoleDO selectByCode(String code, @Nullable String scopeType, @Nullable String objectType) {
return selectOne(new LambdaQueryWrapperX<RoleDO>()
.eq(RoleDO::getCode, code)
.eq(scopeType != null, RoleDO::getScopeType, scopeType)
.eq(objectType != null, RoleDO::getObjectType, objectType));
} }
default List<RoleDO> selectListByStatus(@Nullable Collection<Integer> statuses) { default List<RoleDO> selectListByStatus(@Nullable Collection<Integer> statuses) {
return selectList(RoleDO::getStatus, statuses); return selectListByStatus(statuses, null, null);
}
default List<RoleDO> selectListByStatus(@Nullable Collection<Integer> statuses,
@Nullable String scopeType,
@Nullable String objectType) {
return selectList(new LambdaQueryWrapperX<RoleDO>()
.inIfPresent(RoleDO::getStatus, statuses)
.eq(scopeType != null, RoleDO::getScopeType, scopeType)
.eq(objectType != null, RoleDO::getObjectType, objectType)
.orderByAsc(RoleDO::getSort));
} }
} }

View File

@@ -29,6 +29,7 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>() return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>()
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()) .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()) .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
.likeIfPresent(AdminUserDO::getCompany, reqVO.getCompany())
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()) .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime()) .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(AdminUserDO::getDeptId, deptIds) .inIfPresent(AdminUserDO::getDeptId, deptIds)

View File

@@ -0,0 +1,76 @@
package com.njcn.rdms.module.system.dal.mysql.user;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.system.controller.admin.user.vo.userManagementRelation.UserManagementRelationQueryReqVO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户管理链路 Mapper 接口
*
* 提供用户管理链路表的数据访问操作
* 继承 BaseMapperX 获得基础的 CRUD 功能
*
* @author hongawen
*/
@Mapper
public interface UserManagementRelationMapper extends BaseMapperX<UserManagementRelationDO> {
/**
* 根据查询条件查询用户管理链路列表
*
* 支持的查询条件:
* - managerUserId管理者用户ID精确匹配
* - subordinateUserId被管理用户ID精确匹配
*
* 排序规则按主键ID降序排列
*
* @param reqVO 查询条件VO
* @return 用户管理链路DO列表
*/
default List<UserManagementRelationDO> selectList(UserManagementRelationQueryReqVO reqVO) {
LocalDateTime now = LocalDateTime.now();
return selectList(new LambdaQueryWrapperX<UserManagementRelationDO>()
.eqIfPresent(UserManagementRelationDO::getManagerUserId, reqVO.getManagerUserId())
.eqIfPresent(UserManagementRelationDO::getSubordinateUserId, reqVO.getSubordinateUserId())
.orderByDesc(UserManagementRelationDO::getId)
// (from IS NULL OR from <= now)
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveFrom)
.or().le(UserManagementRelationDO::getEffectiveFrom, now))
// (until IS NULL OR until >= now)
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveUntil)
.or().ge(UserManagementRelationDO::getEffectiveUntil, now))
);
}
/**
* 根据管理者用户ID查询其下属关系列表
*
* 查询指定用户作为管理者时的所有管理链路记录
* 用于获取某个用户的所有直接下属
*
* @param managerUserId 管理者用户ID
* @return 用户管理链路DO列表
*/
default List<UserManagementRelationDO> selectListByManagerUserId(Long managerUserId) {
return selectList(UserManagementRelationDO::getManagerUserId, managerUserId);
}
/**
* 根据被管理者用户ID查询其上级关系列表
*
* 查询指定用户作为被管理者时的所有管理链路记录
* 用于获取某个用户的所有直接上级
*
* @param subordinateUserId 被管理者用户ID
* @return 用户管理链路DO列表
*/
default List<UserManagementRelationDO> selectListBySubordinateUserId(Long subordinateUserId) {
return selectList(UserManagementRelationDO::getSubordinateUserId, subordinateUserId);
}
}

View File

@@ -2,13 +2,16 @@ package com.njcn.rdms.module.system.service.dept;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.njcn.rdms.framework.common.enums.CommonStatusEnum; import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils; import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.controller.admin.dept.vo.post.PostPageReqVO; import com.njcn.rdms.module.system.controller.admin.dept.vo.post.PostPageReqVO;
import com.njcn.rdms.module.system.controller.admin.dept.vo.post.PostSaveReqVO; import com.njcn.rdms.module.system.controller.admin.dept.vo.post.PostSaveReqVO;
import com.njcn.rdms.module.system.dal.dataobject.dept.PostDO; import com.njcn.rdms.module.system.dal.dataobject.dept.PostDO;
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
import com.njcn.rdms.module.system.dal.mysql.dept.PostMapper; import com.njcn.rdms.module.system.dal.mysql.dept.PostMapper;
import com.njcn.rdms.module.system.dal.mysql.user.AdminUserMapper;
import com.njcn.rdms.module.system.enums.dept.PostTypeEnum; import com.njcn.rdms.module.system.enums.dept.PostTypeEnum;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -36,6 +39,9 @@ public class PostServiceImpl implements PostService {
@Resource @Resource
private PostMapper postMapper; private PostMapper postMapper;
@Resource
private AdminUserMapper userMapper;
@Override @Override
public Long createPost(PostSaveReqVO createReqVO) { public Long createPost(PostSaveReqVO createReqVO) {
// 校验正确性 // 校验正确性
@@ -52,6 +58,10 @@ public class PostServiceImpl implements PostService {
// 校验正确性 // 校验正确性
validatePostForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getName(), updateReqVO.getCode(), updateReqVO.getPostType()); validatePostForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getName(), updateReqVO.getCode(), updateReqVO.getPostType());
//如果前端想要禁用,则去校验能否被禁用
if (updateReqVO.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) {
VerifyDoDisable(updateReqVO.getId());
}
// 更新岗位 // 更新岗位
PostDO updateObj = BeanUtils.toBean(updateReqVO, PostDO.class); PostDO updateObj = BeanUtils.toBean(updateReqVO, PostDO.class);
postMapper.updateById(updateObj); postMapper.updateById(updateObj);
@@ -130,6 +140,41 @@ public class PostServiceImpl implements PostService {
} }
} }
private void VerifyDoDisable(Long id) {
/*
通过岗位id去检查是否有用户在使用该岗位position_id = id
1.只查deleted = 0的即没被删除的用户
2.无论用户是被禁用还是正常的status = 0 | 1只要有用户使用该岗位则不能被禁用
*/
QueryWrapper<AdminUserDO> wrapper = new QueryWrapper<>();
wrapper.eq("deleted", false)
.eq("position_id", id);
Long res = userMapper.selectCount(wrapper);
if (res > 0) {
throw exception(POST_DISABLE_NOT_ALLOWED);
}
}
@Override
public void validatePostList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
// 获得岗位信息
List<PostDO> posts = postMapper.selectByIds(ids);
Map<Long, PostDO> postMap = convertMap(posts, PostDO::getId);
// 校验
ids.forEach(id -> {
PostDO post = postMap.get(id);
if (post == null) {
throw exception(POST_NOT_FOUND);
}
if (!CommonStatusEnum.ENABLE.getStatus().equals(post.getStatus())) {
throw exception(POST_NOT_ENABLE, post.getName());
}
});
}
@Override @Override
public List<PostDO> getPostList(Collection<Long> ids) { public List<PostDO> getPostList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) { if (CollUtil.isEmpty(ids)) {
@@ -152,24 +197,4 @@ public class PostServiceImpl implements PostService {
public PostDO getPost(Long id) { public PostDO getPost(Long id) {
return postMapper.selectById(id); return postMapper.selectById(id);
} }
@Override
public void validatePostList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
// 获得岗位信息
List<PostDO> posts = postMapper.selectByIds(ids);
Map<Long, PostDO> postMap = convertMap(posts, PostDO::getId);
// 校验
ids.forEach(id -> {
PostDO post = postMap.get(id);
if (post == null) {
throw exception(POST_NOT_FOUND);
}
if (!CommonStatusEnum.ENABLE.getStatus().equals(post.getStatus())) {
throw exception(POST_NOT_ENABLE, post.getName());
}
});
}
} }

View File

@@ -23,12 +23,42 @@ public interface MenuService {
List<MenuDO> getMenuList(MenuListReqVO reqVO); List<MenuDO> getMenuList(MenuListReqVO reqVO);
/**
* 获得指定作用域下的菜单列表
*
* @param reqVO 菜单查询条件
* @param scopeType 作用域类型
* @param objectType 对象类型
* @return 菜单列表
*/
List<MenuDO> getMenuList(MenuListReqVO reqVO, String scopeType, String objectType);
List<Long> getMenuIdListByPermissionFromCache(String permission); List<Long> getMenuIdListByPermissionFromCache(String permission);
/**
* 从缓存中获得指定作用域下的权限菜单编号集合
*
* @param permission 权限标识
* @param scopeType 作用域类型
* @param objectType 对象类型
* @return 菜单编号集合
*/
List<Long> getMenuIdListByPermissionFromCache(String permission, String scopeType, String objectType);
MenuDO getMenu(Long id); MenuDO getMenu(Long id);
List<MenuDO> getMenuList(Collection<Long> ids); List<MenuDO> getMenuList(Collection<Long> ids);
/**
* 获得指定作用域下的菜单列表
*
* @param ids 菜单编号数组
* @param scopeType 作用域类型
* @param objectType 对象类型
* @return 菜单列表
*/
List<MenuDO> getMenuList(Collection<Long> ids, String scopeType, String objectType);
/** /**
* 校验菜单们是否有效。如下情况,视为无效: * 校验菜单们是否有效。如下情况,视为无效:
* 1. 菜单编号不存在 * 1. 菜单编号不存在
@@ -38,4 +68,13 @@ public interface MenuService {
*/ */
void validateMenuList(Collection<Long> ids); void validateMenuList(Collection<Long> ids);
/**
* 校验指定作用域下的菜单们是否有效
*
* @param ids 菜单编号数组
* @param scopeType 作用域类型
* @param objectType 对象类型
*/
void validateMenuList(Collection<Long> ids, String scopeType, String objectType);
} }

View File

@@ -14,6 +14,7 @@ import com.njcn.rdms.module.system.dal.mysql.permission.MenuMapper;
import com.njcn.rdms.module.system.dal.redis.RedisKeyConstants; import com.njcn.rdms.module.system.dal.redis.RedisKeyConstants;
import com.njcn.rdms.module.system.enums.permission.MenuRouteKindEnum; import com.njcn.rdms.module.system.enums.permission.MenuRouteKindEnum;
import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum; import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@@ -25,6 +26,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.*; import java.util.*;
import java.util.Objects;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertList; import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertList;
@@ -41,6 +43,10 @@ import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.*;
@Slf4j @Slf4j
public class MenuServiceImpl implements MenuService { public class MenuServiceImpl implements MenuService {
private static final String GLOBAL_SCOPE_TYPE = PermissionScopeTypeEnum.GLOBAL.getScopeType();
private static final String OBJECT_SCOPE_TYPE = PermissionScopeTypeEnum.OBJECT.getScopeType();
private static final String GLOBAL_OBJECT_TYPE = PermissionScopeTypeEnum.GLOBAL_OBJECT_TYPE;
@Resource @Resource
private MenuMapper menuMapper; private MenuMapper menuMapper;
@Resource @Resource
@@ -48,18 +54,23 @@ public class MenuServiceImpl implements MenuService {
@Override @Override
@CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#createReqVO.permission", @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, allEntries = true)
condition = "#createReqVO.permission != null")
public Long createMenu(MenuSaveVO createReqVO) { public Long createMenu(MenuSaveVO createReqVO) {
String scopeType = StrUtil.blankToDefault(StrUtil.trim(createReqVO.getScopeType()), GLOBAL_SCOPE_TYPE);
String objectType = OBJECT_SCOPE_TYPE.equals(scopeType) ? StrUtil.trim(createReqVO.getObjectType()) : GLOBAL_OBJECT_TYPE;
// 校验父菜单存在 // 校验父菜单存在
validateParentMenu(createReqVO.getParentId(), null); validateParentMenu(createReqVO.getParentId(), null, scopeType, objectType);
// 校验菜单(自己) // 校验菜单(自己)
validateMenuName(createReqVO.getParentId(), createReqVO.getName(), null); validateMenuName(createReqVO.getParentId(), createReqVO.getName(), null,
validateMenuComponentName(createReqVO.getComponentName(), null); scopeType, objectType);
validateMenuComponentName(createReqVO.getComponentName(), null,
scopeType, objectType);
validateMenuRoute(createReqVO); validateMenuRoute(createReqVO);
// 插入数据库 // 插入数据库
MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class); MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class);
menu.setScopeType(scopeType);
menu.setObjectType(objectType);
initMenuProperty(menu); initMenuProperty(menu);
menuMapper.insert(menu); menuMapper.insert(menu);
// 返回 // 返回
@@ -71,18 +82,24 @@ public class MenuServiceImpl implements MenuService {
allEntries = true) // allEntries 清空所有缓存,因为 permission 如果变更,涉及到新老两个 permission。直接清理简单有效 allEntries = true) // allEntries 清空所有缓存,因为 permission 如果变更,涉及到新老两个 permission。直接清理简单有效
public void updateMenu(MenuSaveVO updateReqVO) { public void updateMenu(MenuSaveVO updateReqVO) {
// 校验更新的菜单是否存在 // 校验更新的菜单是否存在
if (menuMapper.selectById(updateReqVO.getId()) == null) { MenuDO menu = menuMapper.selectById(updateReqVO.getId());
if (menu == null) {
throw exception(MENU_NOT_EXISTS); throw exception(MENU_NOT_EXISTS);
} }
// 校验父菜单存在 // 校验父菜单存在
validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId()); validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId(),
menu.getScopeType(), menu.getObjectType());
// 校验菜单(自己) // 校验菜单(自己)
validateMenuName(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId()); validateMenuName(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId(),
validateMenuComponentName(updateReqVO.getComponentName(), updateReqVO.getId()); menu.getScopeType(), menu.getObjectType());
validateMenuComponentName(updateReqVO.getComponentName(), updateReqVO.getId(),
menu.getScopeType(), menu.getObjectType());
validateMenuRoute(updateReqVO); validateMenuRoute(updateReqVO);
// 更新到数据库 // 更新到数据库
MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class); MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class);
updateObj.setScopeType(menu.getScopeType());
updateObj.setObjectType(menu.getObjectType());
initMenuProperty(updateObj); initMenuProperty(updateObj);
menuMapper.updateById(updateObj); menuMapper.updateById(updateObj);
} }
@@ -129,7 +146,7 @@ public class MenuServiceImpl implements MenuService {
@Override @Override
public List<MenuDO> getMenuList() { public List<MenuDO> getMenuList() {
return menuMapper.selectList(); return getMenuList(new MenuListReqVO(), GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
} }
@Override @Override
@@ -180,13 +197,25 @@ public class MenuServiceImpl implements MenuService {
@Override @Override
public List<MenuDO> getMenuList(MenuListReqVO reqVO) { public List<MenuDO> getMenuList(MenuListReqVO reqVO) {
return menuMapper.selectList(reqVO); return getMenuList(reqVO, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
public List<MenuDO> getMenuList(MenuListReqVO reqVO, String scopeType, String objectType) {
return menuMapper.selectList(reqVO, scopeType, objectType);
} }
@Override @Override
@Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission") @Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission")
public List<Long> getMenuIdListByPermissionFromCache(String permission) { public List<Long> getMenuIdListByPermissionFromCache(String permission) {
List<MenuDO> menus = menuMapper.selectListByPermission(permission); return getMenuIdListByPermissionFromCache(permission, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
@Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST,
key = "#permission + ':' + #scopeType + ':' + #objectType")
public List<Long> getMenuIdListByPermissionFromCache(String permission, String scopeType, String objectType) {
List<MenuDO> menus = menuMapper.selectListByPermission(permission, scopeType, objectType);
return convertList(menus, MenuDO::getId); return convertList(menus, MenuDO::getId);
} }
@@ -201,11 +230,28 @@ public class MenuServiceImpl implements MenuService {
if (CollUtil.isEmpty(ids)) { if (CollUtil.isEmpty(ids)) {
return Lists.newArrayList(); return Lists.newArrayList();
} }
return menuMapper.selectByIds(ids); List<MenuDO> menus = menuMapper.selectByIds(ids);
menus.removeIf(menu -> !matchesScope(menu, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE));
return menus;
}
@Override
public List<MenuDO> getMenuList(Collection<Long> ids, String scopeType, String objectType) {
if (CollUtil.isEmpty(ids)) {
return Lists.newArrayList();
}
List<MenuDO> menus = menuMapper.selectByIds(ids);
menus.removeIf(menu -> !matchesScope(menu, scopeType, objectType));
return menus;
} }
@Override @Override
public void validateMenuList(Collection<Long> ids) { public void validateMenuList(Collection<Long> ids) {
validateMenuList(ids, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
public void validateMenuList(Collection<Long> ids, String scopeType, String objectType) {
if (CollUtil.isEmpty(ids)) { if (CollUtil.isEmpty(ids)) {
return; return;
} }
@@ -216,6 +262,9 @@ public class MenuServiceImpl implements MenuService {
if (menu == null) { if (menu == null) {
throw exception(MENU_NOT_EXISTS); throw exception(MENU_NOT_EXISTS);
} }
if (!matchesScope(menu, scopeType, objectType)) {
throw exception(MENU_SCOPE_NOT_MATCH, menu.getName());
}
if (CommonStatusEnum.isDisable(menu.getStatus())) { if (CommonStatusEnum.isDisable(menu.getStatus())) {
throw exception(MENU_NOT_ENABLE, menu.getName()); throw exception(MENU_NOT_ENABLE, menu.getName());
} }
@@ -234,6 +283,11 @@ public class MenuServiceImpl implements MenuService {
*/ */
@VisibleForTesting @VisibleForTesting
void validateParentMenu(Long parentId, Long childId) { void validateParentMenu(Long parentId, Long childId) {
validateParentMenu(parentId, childId, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@VisibleForTesting
void validateParentMenu(Long parentId, Long childId, String scopeType, String objectType) {
if (parentId == null || ID_ROOT.equals(parentId)) { if (parentId == null || ID_ROOT.equals(parentId)) {
return; return;
} }
@@ -246,6 +300,9 @@ public class MenuServiceImpl implements MenuService {
if (menu == null) { if (menu == null) {
throw exception(MENU_PARENT_NOT_EXISTS); throw exception(MENU_PARENT_NOT_EXISTS);
} }
if (!matchesScope(menu, scopeType, objectType)) {
throw exception(MENU_SCOPE_NOT_MATCH, menu.getName());
}
// 父菜单必须是目录或者菜单类型 // 父菜单必须是目录或者菜单类型
if (!MenuTypeEnum.DIR.getType().equals(menu.getType()) if (!MenuTypeEnum.DIR.getType().equals(menu.getType())
&& !MenuTypeEnum.MENU.getType().equals(menu.getType())) { && !MenuTypeEnum.MENU.getType().equals(menu.getType())) {
@@ -264,7 +321,12 @@ public class MenuServiceImpl implements MenuService {
*/ */
@VisibleForTesting @VisibleForTesting
void validateMenuName(Long parentId, String name, Long id) { void validateMenuName(Long parentId, String name, Long id) {
MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); validateMenuName(parentId, name, id, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@VisibleForTesting
void validateMenuName(Long parentId, String name, Long id, String scopeType, String objectType) {
MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name, scopeType, objectType);
if (menu == null) { if (menu == null) {
return; return;
} }
@@ -285,10 +347,15 @@ public class MenuServiceImpl implements MenuService {
*/ */
@VisibleForTesting @VisibleForTesting
void validateMenuComponentName(String componentName, Long id) { void validateMenuComponentName(String componentName, Long id) {
validateMenuComponentName(componentName, id, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@VisibleForTesting
void validateMenuComponentName(String componentName, Long id, String scopeType, String objectType) {
if (StrUtil.isBlank(componentName)) { if (StrUtil.isBlank(componentName)) {
return; return;
} }
MenuDO menu = menuMapper.selectByComponentName(componentName); MenuDO menu = menuMapper.selectByComponentName(componentName, scopeType, objectType);
if (menu == null) { if (menu == null) {
return; return;
} }
@@ -364,4 +431,10 @@ public class MenuServiceImpl implements MenuService {
return routeKindEnum != null ? routeKindEnum.getKind() : null; return routeKindEnum != null ? routeKindEnum.getKind() : null;
} }
private boolean matchesScope(MenuDO menu, String scopeType, String objectType) {
return menu != null
&& (scopeType == null || Objects.equals(scopeType, menu.getScopeType()))
&& (objectType == null || Objects.equals(objectType, menu.getObjectType()));
}
} }

View File

@@ -73,6 +73,18 @@ public interface PermissionService {
*/ */
Set<Long> getRoleMenuListByRoleId(Collection<Long> roleIds); Set<Long> getRoleMenuListByRoleId(Collection<Long> roleIds);
/**
* 获得指定作用域下的角色们拥有的菜单编号集合
*
* @param roleIds 角色编号数组
* @param scopeType 作用域类型
* @param objectType 对象类型
* @return 菜单编号集合
*/
default Set<Long> getRoleMenuListByRoleId(Collection<Long> roleIds, String scopeType, String objectType) {
return getRoleMenuListByRoleId(roleIds);
}
/** /**
* 获得拥有指定菜单的角色编号数组,从缓存中获取 * 获得拥有指定菜单的角色编号数组,从缓存中获取
* *

View File

@@ -13,6 +13,7 @@ import com.njcn.rdms.module.system.dal.dataobject.permission.UserRoleDO;
import com.njcn.rdms.module.system.dal.mysql.permission.RoleMenuMapper; import com.njcn.rdms.module.system.dal.mysql.permission.RoleMenuMapper;
import com.njcn.rdms.module.system.dal.mysql.permission.UserRoleMapper; import com.njcn.rdms.module.system.dal.mysql.permission.UserRoleMapper;
import com.njcn.rdms.module.system.dal.redis.RedisKeyConstants; import com.njcn.rdms.module.system.dal.redis.RedisKeyConstants;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.service.user.AdminUserService; import com.njcn.rdms.module.system.service.user.AdminUserService;
import com.baomidou.dynamic.datasource.annotation.DSTransactional; import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
@@ -38,6 +39,9 @@ import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.con
@Slf4j @Slf4j
public class PermissionServiceImpl implements PermissionService { public class PermissionServiceImpl implements PermissionService {
private static final String GLOBAL_SCOPE_TYPE = PermissionScopeTypeEnum.GLOBAL.getScopeType();
private static final String GLOBAL_OBJECT_TYPE = PermissionScopeTypeEnum.GLOBAL_OBJECT_TYPE;
@Resource @Resource
private RoleMenuMapper roleMenuMapper; private RoleMenuMapper roleMenuMapper;
@Resource @Resource
@@ -82,12 +86,13 @@ public class PermissionServiceImpl implements PermissionService {
* @return 是否拥有 * @return 是否拥有
*/ */
private boolean hasAnyPermission(List<RoleDO> roles, String permission) { private boolean hasAnyPermission(List<RoleDO> roles, String permission) {
List<Long> menuIds = menuService.getMenuIdListByPermissionFromCache(permission); List<Long> menuIds = menuService.getMenuIdListByPermissionFromCache(permission,
GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
// 采用严格模式,如果权限找不到对应的 Menu 的话,也认为没有权限 // 采用严格模式,如果权限找不到对应的 Menu 的话,也认为没有权限
if (CollUtil.isEmpty(menuIds)) { if (CollUtil.isEmpty(menuIds)) {
return false; return false;
} }
List<MenuDO> menus = getEnablePermissionMenus(menuIds); List<MenuDO> menus = getEnablePermissionMenus(menuIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
if (CollUtil.isEmpty(menus)) { if (CollUtil.isEmpty(menus)) {
return false; return false;
} }
@@ -108,12 +113,12 @@ public class PermissionServiceImpl implements PermissionService {
/** /**
* 加载权限菜单自身及其父链后,再统一过滤禁用节点,避免仅查询按钮节点时误判父菜单缺失。 * 加载权限菜单自身及其父链后,再统一过滤禁用节点,避免仅查询按钮节点时误判父菜单缺失。
*/ */
private List<MenuDO> getEnablePermissionMenus(Collection<Long> menuIds) { private List<MenuDO> getEnablePermissionMenus(Collection<Long> menuIds, String scopeType, String objectType) {
Set<Long> targetMenuIds = new HashSet<>(menuIds); Set<Long> targetMenuIds = new HashSet<>(menuIds);
Map<Long, MenuDO> menuMap = new LinkedHashMap<>(); Map<Long, MenuDO> menuMap = new LinkedHashMap<>();
Set<Long> currentIds = new HashSet<>(menuIds); Set<Long> currentIds = new HashSet<>(menuIds);
while (CollUtil.isNotEmpty(currentIds)) { while (CollUtil.isNotEmpty(currentIds)) {
List<MenuDO> currentMenus = menuService.getMenuList(currentIds); List<MenuDO> currentMenus = menuService.getMenuList(currentIds, scopeType, objectType);
if (CollUtil.isEmpty(currentMenus)) { if (CollUtil.isEmpty(currentMenus)) {
break; break;
} }
@@ -131,15 +136,15 @@ public class PermissionServiceImpl implements PermissionService {
/** /**
* 为已选菜单补齐父链,避免只授权子菜单或按钮时,权限树缺少上级节点。 * 为已选菜单补齐父链,避免只授权子菜单或按钮时,权限树缺少上级节点。
*/ */
private Set<Long> expandMenuIdsWithAncestors(Collection<Long> menuIds) { private Set<Long> expandMenuIdsWithAncestors(Collection<Long> menuIds, String scopeType, String objectType) {
Set<Long> results = new LinkedHashSet<>(menuIds); Set<Long> results = new LinkedHashSet<>(menuIds);
menuIds.forEach(menuId -> { menuIds.forEach(menuId -> {
MenuDO menu = menuService.getMenu(menuId); MenuDO menu = getMenu(menuId, scopeType, objectType);
while (menu != null && !MenuDO.ID_ROOT.equals(menu.getParentId())) { while (menu != null && !MenuDO.ID_ROOT.equals(menu.getParentId())) {
if (!results.add(menu.getParentId())) { if (!results.add(menu.getParentId())) {
break; break;
} }
menu = menuService.getMenu(menu.getParentId()); menu = getMenu(menu.getParentId(), scopeType, objectType);
} }
}); });
return results; return results;
@@ -174,12 +179,14 @@ public class PermissionServiceImpl implements PermissionService {
allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快 allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快
}) })
public void assignRoleMenu(Long roleId, Set<Long> menuIds) { public void assignRoleMenu(Long roleId, Set<Long> menuIds) {
roleService.validateRoleList(Collections.singleton(roleId)); roleService.validateRoleList(Collections.singleton(roleId), GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
menuService.validateMenuList(menuIds); RoleDO role = roleService.getRole(roleId);
menuService.validateMenuList(menuIds, role.getScopeType(), role.getObjectType());
// 获得角色拥有菜单编号 // 获得角色拥有菜单编号
Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId);
// 计算新增和删除的菜单编号 // 计算新增和删除的菜单编号
Set<Long> menuIdList = expandMenuIdsWithAncestors(CollUtil.emptyIfNull(menuIds)); Set<Long> menuIdList = expandMenuIdsWithAncestors(CollUtil.emptyIfNull(menuIds),
role.getScopeType(), role.getObjectType());
Collection<Long> createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds); Collection<Long> createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds);
Collection<Long> deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList); Collection<Long> deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList);
// 执行新增和删除。对于已经授权的菜单,不用做任何处理 // 执行新增和删除。对于已经授权的菜单,不用做任何处理
@@ -222,15 +229,24 @@ public class PermissionServiceImpl implements PermissionService {
if (CollUtil.isEmpty(roleIds)) { if (CollUtil.isEmpty(roleIds)) {
return Collections.emptySet(); return Collections.emptySet();
} }
roleService.validateRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
return getRoleMenuListByRoleId(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
// 如果是管理员的情况下,获取全部菜单编号 @Override
if (roleService.hasAnySuperAdmin(roleIds)) { public Set<Long> getRoleMenuListByRoleId(Collection<Long> roleIds, String scopeType, String objectType) {
return convertSet(menuService.filterDisableMenus(menuService.getMenuList()), MenuDO::getId); if (CollUtil.isEmpty(roleIds)) {
return Collections.emptySet();
} }
// 如果是非管理员的情况下,仅返回当前仍然有效的菜单,并补齐其父链
Set<Long> menuIds = convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId); // 统一按角色实际授权返回当前仍然有效的菜单,并补齐其父链
List<MenuDO> menus = menuService.filterDisableMenus(menuService.getMenuList(menuIds)); Set<Long> scopedRoleIds = convertSet(roleService.getRoleList(roleIds, scopeType, objectType), RoleDO::getId);
return expandMenuIdsWithAncestors(convertSet(menus, MenuDO::getId)); if (CollUtil.isEmpty(scopedRoleIds)) {
return Collections.emptySet();
}
Set<Long> menuIds = convertSet(roleMenuMapper.selectListByRoleId(scopedRoleIds), RoleMenuDO::getMenuId);
List<MenuDO> menus = menuService.filterDisableMenus(menuService.getMenuList(menuIds, scopeType, objectType));
return expandMenuIdsWithAncestors(convertSet(menus, MenuDO::getId), scopeType, objectType);
} }
@Override @Override
@@ -246,14 +262,16 @@ public class PermissionServiceImpl implements PermissionService {
@CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public void assignUserRole(Long userId, Set<Long> roleIds) { public void assignUserRole(Long userId, Set<Long> roleIds) {
userService.validateUserList(Collections.singleton(userId)); userService.validateUserList(Collections.singleton(userId));
roleService.validateRoleList(roleIds); roleService.validateRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
// 获得角色拥有角色编号 // 获得角色拥有角色编号
Set<Long> dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId), Set<Long> dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId),
UserRoleDO::getRoleId); UserRoleDO::getRoleId);
Set<Long> dbGlobalRoleIds = convertSet(
roleService.getRoleList(dbRoleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE), RoleDO::getId);
// 计算新增和删除的角色编号 // 计算新增和删除的角色编号
Set<Long> roleIdList = CollUtil.emptyIfNull(roleIds); Set<Long> roleIdList = CollUtil.emptyIfNull(roleIds);
Collection<Long> createRoleIds = CollUtil.subtract(roleIdList, dbRoleIds); Collection<Long> createRoleIds = CollUtil.subtract(roleIdList, dbGlobalRoleIds);
Collection<Long> deleteMenuIds = CollUtil.subtract(dbRoleIds, roleIdList); Collection<Long> deleteRoleIds = CollUtil.subtract(dbGlobalRoleIds, roleIdList);
// 执行新增和删除。对于已经授权的角色,不用做任何处理 // 执行新增和删除。对于已经授权的角色,不用做任何处理
if (!CollectionUtil.isEmpty(createRoleIds)) { if (!CollectionUtil.isEmpty(createRoleIds)) {
userRoleMapper.insertBatch(CollectionUtils.convertList(createRoleIds, roleId -> { userRoleMapper.insertBatch(CollectionUtils.convertList(createRoleIds, roleId -> {
@@ -263,8 +281,8 @@ public class PermissionServiceImpl implements PermissionService {
return entity; return entity;
})); }));
} }
if (!CollectionUtil.isEmpty(deleteMenuIds)) { if (!CollectionUtil.isEmpty(deleteRoleIds)) {
userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteMenuIds); userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteRoleIds);
} }
} }
@@ -277,7 +295,7 @@ public class PermissionServiceImpl implements PermissionService {
@Override @Override
public Set<Long> getUserRoleIdListByUserId(Long userId) { public Set<Long> getUserRoleIdListByUserId(Long userId) {
Set<Long> roleIds = getRawUserRoleIdListByUserId(userId); Set<Long> roleIds = getRawUserRoleIdListByUserId(userId);
List<RoleDO> roles = roleService.getRoleList(roleIds); List<RoleDO> roles = roleService.getRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus()));
return convertSet(roles, RoleDO::getId); return convertSet(roles, RoleDO::getId);
} }
@@ -285,7 +303,8 @@ public class PermissionServiceImpl implements PermissionService {
@Override @Override
@Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") @Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public Set<Long> getUserRoleIdListByUserIdFromCache(Long userId) { public Set<Long> getUserRoleIdListByUserIdFromCache(Long userId) {
return getRawUserRoleIdListByUserId(userId); Set<Long> roleIds = getRawUserRoleIdListByUserId(userId);
return convertSet(roleService.getRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE), RoleDO::getId);
} }
@Override @Override
@@ -314,6 +333,11 @@ public class PermissionServiceImpl implements PermissionService {
* *
* @return 自己 * @return 自己
*/ */
private MenuDO getMenu(Long menuId, String scopeType, String objectType) {
List<MenuDO> menus = menuService.getMenuList(Collections.singleton(menuId), scopeType, objectType);
return CollUtil.isEmpty(menus) ? null : menus.get(0);
}
private PermissionServiceImpl getSelf() { private PermissionServiceImpl getSelf() {
return SpringUtil.getBean(getClass()); return SpringUtil.getBean(getClass());
} }

View File

@@ -70,6 +70,16 @@ public interface RoleService {
*/ */
List<RoleDO> getRoleList(Collection<Long> ids); List<RoleDO> getRoleList(Collection<Long> ids);
/**
* 获得指定作用域下的角色列表
*
* @param ids 角色编号数组
* @param scopeType 作用域类型
* @param objectType 对象类型
* @return 角色列表
*/
List<RoleDO> getRoleList(Collection<Long> ids, String scopeType, String objectType);
/** /**
* 获得角色数组,从缓存中 * 获得角色数组,从缓存中
* *
@@ -86,6 +96,16 @@ public interface RoleService {
*/ */
List<RoleDO> getRoleListByStatus(Collection<Integer> statuses); List<RoleDO> getRoleListByStatus(Collection<Integer> statuses);
/**
* 获得指定作用域下的角色列表
*
* @param statuses 筛选的状态
* @param scopeType 作用域类型
* @param objectType 对象类型
* @return 角色列表
*/
List<RoleDO> getRoleListByStatus(Collection<Integer> statuses, String scopeType, String objectType);
/** /**
* 获得所有角色列表 * 获得所有角色列表
* *
@@ -101,6 +121,16 @@ public interface RoleService {
*/ */
PageResult<RoleDO> getRolePage(RolePageReqVO reqVO); PageResult<RoleDO> getRolePage(RolePageReqVO reqVO);
/**
* 获得指定作用域下的角色分页
*
* @param reqVO 角色分页查询
* @param scopeType 作用域类型
* @param objectType 对象类型
* @return 角色分页结果
*/
PageResult<RoleDO> getRolePage(RolePageReqVO reqVO, String scopeType, String objectType);
/** /**
* 判断角色编号数组中,是否有管理员 * 判断角色编号数组中,是否有管理员
* *
@@ -118,4 +148,13 @@ public interface RoleService {
*/ */
void validateRoleList(Collection<Long> ids); void validateRoleList(Collection<Long> ids);
/**
* 校验指定作用域下的角色们是否有效
*
* @param ids 角色编号数组
* @param scopeType 作用域类型
* @param objectType 对象类型
*/
void validateRoleList(Collection<Long> ids, String scopeType, String objectType);
} }

View File

@@ -4,7 +4,9 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.mzt.logapi.context.LogRecordContext; import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction; import com.mzt.logapi.service.impl.DiffParseFunction;
@@ -16,8 +18,11 @@ import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RolePageReqVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RolePageReqVO;
import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import com.njcn.rdms.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO; import com.njcn.rdms.module.system.dal.dataobject.permission.RoleDO;
import com.njcn.rdms.module.system.dal.dataobject.permission.UserRoleDO;
import com.njcn.rdms.module.system.dal.mysql.permission.RoleMapper; import com.njcn.rdms.module.system.dal.mysql.permission.RoleMapper;
import com.njcn.rdms.module.system.dal.mysql.permission.UserRoleMapper;
import com.njcn.rdms.module.system.dal.redis.RedisKeyConstants; import com.njcn.rdms.module.system.dal.redis.RedisKeyConstants;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.enums.permission.RoleCodeEnum; import com.njcn.rdms.module.system.enums.permission.RoleCodeEnum;
import com.njcn.rdms.module.system.enums.permission.RoleTypeEnum; import com.njcn.rdms.module.system.enums.permission.RoleTypeEnum;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -32,43 +37,45 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertMap; import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.convertMap;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_ADMIN_CODE_ERROR; import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.*;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_CAN_NOT_DELETE_SYSTEM_TYPE_ROLE; import static com.njcn.rdms.module.system.enums.LogRecordConstants.*;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_CODE_DUPLICATE;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_IS_DISABLE;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_NAME_DUPLICATE;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.ROLE_NOT_EXISTS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_CREATE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_CREATE_SUCCESS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_DELETE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_DELETE_SUCCESS;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_UPDATE_SUB_TYPE;
import static com.njcn.rdms.module.system.enums.LogRecordConstants.SYSTEM_ROLE_UPDATE_SUCCESS;
@Service @Service
@Slf4j @Slf4j
public class RoleServiceImpl implements RoleService { public class RoleServiceImpl implements RoleService {
private static final String GLOBAL_SCOPE_TYPE = PermissionScopeTypeEnum.GLOBAL.getScopeType();
private static final String OBJECT_SCOPE_TYPE = PermissionScopeTypeEnum.OBJECT.getScopeType();
private static final String GLOBAL_OBJECT_TYPE = PermissionScopeTypeEnum.GLOBAL_OBJECT_TYPE;
@Resource @Resource
private PermissionService permissionService; private PermissionService permissionService;
@Resource @Resource
private RoleMapper roleMapper; private RoleMapper roleMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@LogRecord(type = SYSTEM_ROLE_TYPE, subType = SYSTEM_ROLE_CREATE_SUB_TYPE, bizNo = "{{#role.id}}", @LogRecord(type = SYSTEM_ROLE_TYPE, subType = SYSTEM_ROLE_CREATE_SUB_TYPE, bizNo = "{{#role.id}}",
success = SYSTEM_ROLE_CREATE_SUCCESS) success = SYSTEM_ROLE_CREATE_SUCCESS)
public Long createRole(RoleSaveReqVO createReqVO, Integer type) { public Long createRole(RoleSaveReqVO createReqVO, Integer type) {
validateRoleDuplicate(createReqVO.getName(), createReqVO.getCode(), null); String scopeType = StrUtil.blankToDefault(StrUtil.trim(createReqVO.getScopeType()), GLOBAL_SCOPE_TYPE);
String objectType = OBJECT_SCOPE_TYPE.equals(scopeType) ? StrUtil.trim(createReqVO.getObjectType()) : GLOBAL_OBJECT_TYPE;
validateRoleDuplicate(createReqVO.getName(), createReqVO.getCode(), null,
scopeType, objectType);
RoleDO role = BeanUtils.toBean(createReqVO, RoleDO.class) RoleDO role = BeanUtils.toBean(createReqVO, RoleDO.class)
.setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType())) .setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType()))
.setStatus(ObjUtil.defaultIfNull(createReqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())); .setStatus(ObjUtil.defaultIfNull(createReqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus()))
.setScopeType(scopeType)
.setObjectType(objectType);
roleMapper.insert(role); roleMapper.insert(role);
LogRecordContext.putVariable("role", role); LogRecordContext.putVariable("role", role);
@@ -82,12 +89,20 @@ public class RoleServiceImpl implements RoleService {
public void updateRole(RoleSaveReqVO updateReqVO) { public void updateRole(RoleSaveReqVO updateReqVO) {
RoleDO role = validateRoleExists(updateReqVO.getId()); RoleDO role = validateRoleExists(updateReqVO.getId());
String effectiveCode = shouldPreserveBuiltInCode(role) ? role.getCode() : updateReqVO.getCode(); String effectiveCode = shouldPreserveBuiltInCode(role) ? role.getCode() : updateReqVO.getCode();
validateRoleDuplicate(updateReqVO.getName(), effectiveCode, updateReqVO.getId()); validateRoleDuplicate(updateReqVO.getName(), effectiveCode, updateReqVO.getId(),
role.getScopeType(), role.getObjectType());
//如果前端想要禁用,则去校验能否被禁用
if (updateReqVO.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) {
VerifyDoDisable(updateReqVO.getId());
}
RoleDO updateObj = BeanUtils.toBean(updateReqVO, RoleDO.class); RoleDO updateObj = BeanUtils.toBean(updateReqVO, RoleDO.class);
if (shouldPreserveBuiltInCode(role)) { if (shouldPreserveBuiltInCode(role)) {
updateObj.setCode(role.getCode()); updateObj.setCode(role.getCode());
} }
updateObj.setScopeType(role.getScopeType());
updateObj.setObjectType(role.getObjectType());
roleMapper.updateById(updateObj); roleMapper.updateById(updateObj);
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(role, RoleSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(role, RoleSaveReqVO.class));
@@ -118,8 +133,28 @@ public class RoleServiceImpl implements RoleService {
ids.forEach(permissionService::processRoleDeleted); ids.forEach(permissionService::processRoleDeleted);
} }
private void VerifyDoDisable(Long id) {
/*
通过角色id去检查是否有用户在使用该角色检查system_user_role表
1.只查deleted = 0的即没被删除的记录
2.无论用户是被禁用还是正常的status = 0 | 1只要有用户使用该角色则不能被禁用
*/
QueryWrapper<UserRoleDO> wrapper = new QueryWrapper<>();
wrapper.eq("deleted", false)
.eq("role_id", id);
Long res = userRoleMapper.selectCount(wrapper);
if (res > 0) {
throw exception(ROLE_DISABLE_NOT_ALLOWED);
}
}
@VisibleForTesting @VisibleForTesting
void validateRoleDuplicate(String name, String code, Long id) { void validateRoleDuplicate(String name, String code, Long id) {
validateRoleDuplicate(name, code, id, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@VisibleForTesting
void validateRoleDuplicate(String name, String code, Long id, String scopeType, String objectType) {
if (RoleCodeEnum.isBuiltIn(code)) { if (RoleCodeEnum.isBuiltIn(code)) {
if (id == null) { if (id == null) {
throw exception(ROLE_ADMIN_CODE_ERROR, code); throw exception(ROLE_ADMIN_CODE_ERROR, code);
@@ -130,7 +165,7 @@ public class RoleServiceImpl implements RoleService {
} }
} }
RoleDO role = roleMapper.selectByName(name); RoleDO role = roleMapper.selectByName(name, scopeType, objectType);
if (role != null && !role.getId().equals(id)) { if (role != null && !role.getId().equals(id)) {
throw exception(ROLE_NAME_DUPLICATE, name); throw exception(ROLE_NAME_DUPLICATE, name);
} }
@@ -138,7 +173,7 @@ public class RoleServiceImpl implements RoleService {
if (!StringUtils.hasText(code)) { if (!StringUtils.hasText(code)) {
return; return;
} }
role = roleMapper.selectByCode(code); role = roleMapper.selectByCode(code, scopeType, objectType);
if (role != null && !role.getId().equals(id)) { if (role != null && !role.getId().equals(id)) {
throw exception(ROLE_CODE_DUPLICATE, code); throw exception(ROLE_CODE_DUPLICATE, code);
} }
@@ -181,20 +216,32 @@ public class RoleServiceImpl implements RoleService {
@Override @Override
public List<RoleDO> getRoleListByStatus(Collection<Integer> statuses) { public List<RoleDO> getRoleListByStatus(Collection<Integer> statuses) {
return roleMapper.selectListByStatus(statuses); return getRoleListByStatus(statuses, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
public List<RoleDO> getRoleListByStatus(Collection<Integer> statuses, String scopeType, String objectType) {
return roleMapper.selectListByStatus(statuses, scopeType, objectType);
} }
@Override @Override
public List<RoleDO> getRoleList() { public List<RoleDO> getRoleList() {
return roleMapper.selectList(); return getRoleListByStatus(null, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
} }
@Override @Override
public List<RoleDO> getRoleList(Collection<Long> ids) { public List<RoleDO> getRoleList(Collection<Long> ids) {
return getRoleList(ids, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
public List<RoleDO> getRoleList(Collection<Long> ids, String scopeType, String objectType) {
if (CollectionUtil.isEmpty(ids)) { if (CollectionUtil.isEmpty(ids)) {
return Collections.emptyList(); return Collections.emptyList();
} }
return roleMapper.selectByIds(ids); List<RoleDO> roles = roleMapper.selectByIds(ids);
roles.removeIf(role -> !matchesScope(role, scopeType, objectType));
return roles;
} }
@Override @Override
@@ -208,7 +255,12 @@ public class RoleServiceImpl implements RoleService {
@Override @Override
public PageResult<RoleDO> getRolePage(RolePageReqVO reqVO) { public PageResult<RoleDO> getRolePage(RolePageReqVO reqVO) {
return roleMapper.selectPage(reqVO); return getRolePage(reqVO, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
public PageResult<RoleDO> getRolePage(RolePageReqVO reqVO, String scopeType, String objectType) {
return roleMapper.selectPageByScope(reqVO, scopeType, objectType);
} }
@Override @Override
@@ -225,6 +277,11 @@ public class RoleServiceImpl implements RoleService {
@Override @Override
public void validateRoleList(Collection<Long> ids) { public void validateRoleList(Collection<Long> ids) {
validateRoleList(ids, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
public void validateRoleList(Collection<Long> ids, String scopeType, String objectType) {
if (CollUtil.isEmpty(ids)) { if (CollUtil.isEmpty(ids)) {
return; return;
} }
@@ -235,12 +292,21 @@ public class RoleServiceImpl implements RoleService {
if (role == null) { if (role == null) {
throw exception(ROLE_NOT_EXISTS); throw exception(ROLE_NOT_EXISTS);
} }
if (!matchesScope(role, scopeType, objectType)) {
throw exception(ROLE_SCOPE_NOT_MATCH, role.getName());
}
if (!CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())) { if (!CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())) {
throw exception(ROLE_IS_DISABLE, role.getName()); throw exception(ROLE_IS_DISABLE, role.getName());
} }
}); });
} }
private boolean matchesScope(RoleDO role, String scopeType, String objectType) {
return role != null
&& (scopeType == null || Objects.equals(scopeType, role.getScopeType()))
&& (objectType == null || Objects.equals(objectType, role.getObjectType()));
}
private RoleServiceImpl getSelf() { private RoleServiceImpl getSelf() {
return SpringUtil.getBean(getClass()); return SpringUtil.getBean(getClass());
} }

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