diff --git a/AGENTS.md b/AGENTS.md
index e0a5d67..cd4c152 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -26,10 +26,11 @@
顶层模块:
1. `rdms-system`
-2. `rdms-framework`
-3. `rdms-gateway`
+2. `rdms-project`
+3. `rdms-framework`
+4. `rdms-gateway`
-当前业务实现主要集中在 `rdms-system`,但这只是现阶段结构,不应被理解为长期只保留一个业务模块。
+当前系统域能力主要集中在 `rdms-system`,RDMS 核心交付域能力主要集中在 `rdms-project`,但这只是现阶段结构,不应被理解为长期只保留这两个业务模块。
后续如果新增独立业务服务,例如项目/产品管理模块、工作流模块,应继续沿用当前仓库的模块拆分方式,而不是把所有后续业务长期堆进 `rdms-system`。
@@ -54,6 +55,24 @@
- 如果后续只是给系统域补充新的系统子能力,可以继续在 `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 模块。
@@ -189,7 +208,7 @@ rdms-xxx
- 服务层放在 `service`
- 持久层放在 `dal`
- DTO/VO 转换放在 `convert`
-6. 当前业务代码主要在 `rdms-system`,但这不是永久约束;新增业务能力时,先判断应该落在现有系统域内,还是应建设为新的 `rdms-xxx` 业务模块。
+6. 当前系统域代码主要在 `rdms-system`,RDMS 核心交付域代码主要在 `rdms-project`,但这不是永久约束;新增业务能力时,先判断应该落在现有系统域内、现有项目交付域内,还是应建设为新的 `rdms-xxx` 业务模块。
7. 新增共享能力时,优先扩展现有 `rdms-spring-boot-starter-*` 模块,不要在业务服务里重复堆配置。
8. 修改跨模块使用的 API 时,需要同时更新提供方实现和对应的 `rdms-system-api` 或对应 `rdms-xxx-api` 契约。
9. 除非用户明确要求,否则不执行任何编译、构建、测试、打包或其他会实际运行项目的命令,包括但不限于 `mvn`、启动命令和脚本。
diff --git a/pom.xml b/pom.xml
index b2ad72f..98cba49 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,6 +9,7 @@
pom
rdms-system
+ rdms-project
rdms-framework
rdms-gateway
diff --git a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/RpcConstants.java b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/RpcConstants.java
index 06cded7..0519a75 100644
--- a/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/RpcConstants.java
+++ b/rdms-framework/rdms-common/src/main/java/com/njcn/rdms/framework/common/enums/RpcConstants.java
@@ -26,5 +26,17 @@ public interface RpcConstants {
*/
String SYSTEM_PREFIX = RPC_API_PREFIX + "/system";
+ /**
+ * project 服务名
+ *
+ * 注意,需要保证和 spring.application.name 保持一致
+ */
+ String PROJECT_NAME = "rdms-project-server";
+
+ /**
+ * project 服务的前缀
+ */
+ String PROJECT_PREFIX = RPC_API_PREFIX + "/project";
+
}
diff --git a/rdms-gateway/src/main/resources/application.yaml b/rdms-gateway/src/main/resources/application.yaml
index 646f8bf..36cb726 100644
--- a/rdms-gateway/src/main/resources/application.yaml
+++ b/rdms-gateway/src/main/resources/application.yaml
@@ -49,6 +49,19 @@ spring:
uri: grayLb://rdms-system-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/system/ws/**
+ ## project-server 服务
+ - id: project-admin-api # 路由的编号
+ uri: grayLb://rdms-project-server
+ predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
+ - Path=/admin-api/project/**
+ filters:
+ - RewritePath=/admin-api/project/v3/api-docs, /v3/api-docs
+ - id: project-app-api # 路由的编号
+ uri: grayLb://rdms-project-server
+ predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
+ - Path=/app-api/project/**
+ filters:
+ - RewritePath=/app-api/project/v3/api-docs, /v3/api-docs
## bpm-server 服务
- id: bpm-admin-api # 路由的编号
uri: grayLb://rdms-bpm-server
@@ -76,6 +89,9 @@ knife4j:
- name: system-server
service-name: rdms-system-server
url: /admin-api/system/v3/api-docs
+ - name: project-server
+ service-name: rdms-project-server
+ url: /admin-api/project/v3/api-docs
- name: bpm-server
service-name: bpm-server
url: /admin-api/bpm/v3/api-docs
diff --git a/rdms-project/pom.xml b/rdms-project/pom.xml
new file mode 100644
index 0000000..2905f06
--- /dev/null
+++ b/rdms-project/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+ com.njcn
+ cn-rdms
+ ${revision}
+
+
+ rdms-project
+ pom
+ ${project.artifactId}
+
+ RDMS 项目交付域模块
+ 该模块承载项目集、项目、产品、需求、任务、工单、执行等 RDMS 核心交付业务能力。
+
+
+ rdms-project-boot
+ rdms-project-api
+
+
+
+ 17
+ 17
+ UTF-8
+
+
+
diff --git a/rdms-project/rdms-project-api/pom.xml b/rdms-project/rdms-project-api/pom.xml
new file mode 100644
index 0000000..7fb6b0d
--- /dev/null
+++ b/rdms-project/rdms-project-api/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+ com.njcn
+ rdms-project
+ ${revision}
+
+
+ rdms-project-api
+
+ 项目交付域接口,暴露给其它模块调用
+
+
+
+
+
+ com.njcn
+ rdms-common
+
+
+
+
+ io.swagger.core.v3
+ swagger-annotations
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ true
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+ true
+
+
+
+
+
diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/api/package-info.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/api/package-info.java
new file mode 100644
index 0000000..5d0f51b
--- /dev/null
+++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/api/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Project API 包,定义暴露给其它模块的 API
+ */
+package com.njcn.rdms.module.project.api;
diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ApiConstants.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ApiConstants.java
new file mode 100644
index 0000000..eef0ef9
--- /dev/null
+++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ApiConstants.java
@@ -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() {
+ }
+
+}
diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java
new file mode 100644
index 0000000..0be0d7e
--- /dev/null
+++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java
@@ -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, "产品暂停后仅允许变更产品经理、描述和备注");
+
+}
diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ProjectDictTypeConstants.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ProjectDictTypeConstants.java
new file mode 100644
index 0000000..e26fe3e
--- /dev/null
+++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ProjectDictTypeConstants.java
@@ -0,0 +1,13 @@
+package com.njcn.rdms.module.project.enums;
+
+/**
+ * 项目交付域字典类型常量
+ */
+public interface ProjectDictTypeConstants {
+
+ /**
+ * 产品方向
+ */
+ String PRODUCT_DIRECTION = "rdms_product_direction";
+
+}
diff --git a/rdms-project/rdms-project-boot/pom.xml b/rdms-project/rdms-project-boot/pom.xml
new file mode 100644
index 0000000..b075fca
--- /dev/null
+++ b/rdms-project/rdms-project-boot/pom.xml
@@ -0,0 +1,116 @@
+
+
+ 4.0.0
+
+ com.njcn
+ rdms-project
+ ${revision}
+
+
+ rdms-project-boot
+ 项目交付域功能服务模块
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-env
+
+
+
+
+ com.njcn
+ rdms-project-api
+ ${revision}
+
+
+ com.njcn
+ rdms-system-api
+ ${revision}
+
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-security
+
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-mybatis
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-redis
+
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-rpc
+
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-excel
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+
+ com.njcn
+ rdms-spring-boot-starter-test
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+
+
+ ${project.artifactId}
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+ true
+
+
+
+
+ repackage
+
+
+
+
+
+
+
diff --git a/rdms-project/rdms-project-boot/product/02-产品管理_SQL已确认口径.md b/rdms-project/rdms-project-boot/product/02-产品管理_SQL已确认口径.md
new file mode 100644
index 0000000..00f6ecc
--- /dev/null
+++ b/rdms-project/rdms-project-boot/product/02-产品管理_SQL已确认口径.md
@@ -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 项:主表补终态结果字段,审计日志继续完整留痕
diff --git a/rdms-project/rdms-project-boot/product/02-产品管理_业务设计.md b/rdms-project/rdms-project-boot/product/02-产品管理_业务设计.md
new file mode 100644
index 0000000..d409d7b
--- /dev/null
+++ b/rdms-project/rdms-project-boot/product/02-产品管理_业务设计.md
@@ -0,0 +1,1057 @@
+# 02-产品管理 详细设计
+
+## 0. 文档定位
+
+| 项目 | 内容 |
+|---|---|
+| 文档目标 | 将《设计任务书》7.2 产品管理章节展开为开发、测试、接口、数据库可直接承接的详细设计 |
+| 适用版本 | V0.5 |
+| 设计范围 | 产品主数据、产品团队、产品与项目关系、产品生命周期、轻量需求管理 |
+| 非本期范围 | 产品版本、路线图、评审流、基线、产品文档与附件、工单反馈管理 |
+| 上游来源 | `04-设计阶段/设计任务书/设计任务书_V1.0.md`、`02-产品规划阶段/产品需求文档PRD_V1.0.md`、`02-产品规划阶段/术语与边界对齐_产品-项目集-项目.md`、`04-设计阶段/权限设计/权限设计方案.md` |
+| 下游约束对象 | `04-设计阶段/数据库设计文档.md`、`04-设计阶段/API接口设计文档.md`、`04-设计阶段/业务设计/03-项目管理_业务设计.md`、`04-设计阶段/权限设计/` |
+| 设计原则 | 先固化稳定归属和最小闭环,再扩展增强能力;产品层不承载任务、工时、周报等执行主数据 |
+
+本文档回答 6 个问题:
+
+- V0.5 产品管理到底做哪些页面、动作和字段。
+- 产品、项目、产品团队、产品需求之间怎么关联。
+- 每个动作的前置条件、校验规则、状态流转和审计要求是什么。
+- 产品可见范围、建项目资格、产品团队成员池之间如何衔接。
+- 产品需求如何与项目需求分流,不互相替代。
+- 数据库、接口、权限、测试应按什么口径承接。
+
+## 1. 设计目标与范围
+
+### 1.1 模块目标
+
+- 建立稳定的产品主数据管理能力,作为项目归属的上层业务对象。
+- 形成“产品 -> 项目 -> 执行/任务 -> 工时/周报/统计”的稳定主链路。
+- 为产品侧长期协作提供独立于项目团队的产品团队机制。
+- 为通用需求沉淀提供轻量需求池,不把定制交付需求混入产品侧。
+
+### 1.2 V0.5 包含范围
+
+- 产品创建、编辑、查看、列表查询。
+- 产品经理维护与产品团队管理。
+- 产品启用、暂停、归档、废弃、逻辑删除。
+- 项目创建时强制选择所属产品。
+- 在当前产品上下文中查看关联项目。
+- 产品轻量需求的录入、编辑、查询、基础状态流转、关闭。
+- 产品研发令号按产品年度维护的数据结构预留。
+- 产品需求在 V0.5 仅承接轻量需求池管理;是否进入 `待评审` 由业务判断决定,但不引入正式审批流、工作流引擎和复杂评审编排。
+
+### 1.3 V0.5 不包含范围
+
+- 产品版本管理。
+- 产品路线图。
+- 基于工作流引擎的正式产品评审流、审批流。
+- 复杂产品需求增强管理,包括多级评审编排、会签/或签、退回重提等流程能力。
+- 产品研发令号独立管理界面。
+- 产品基线。
+- 产品文档与附件管理。
+- 工单/反馈与产品需求自动联动。
+- 产品维度统计看板。
+
+### 1.4 当前设计口径
+
+- 产品管理当前不设置产品模板或产品类型正式字段。
+- 产品方向为正式字段,统一通过系统字典承接;字典类型编码统一为 `rdms_product_direction`,业务表字段 `direction_code` 存字典 `value`,初始值为 `embedded`、`power_electronics`、`system_group`。
+- 产品需求支持按业务判断进入 `待评审` 状态;当前版本仅表达业务评审待处理语义,不展开正式评审流程、工作流引擎和复杂评审编排。
+
+## 2. 上下游约束
+
+### 2.1 术语约束
+
+- 产品(Product)是持续交付和维护的业务对象,生命周期长。
+- 项目(Project)是围绕某次明确交付目标建立的执行单元,生命周期短于产品。
+- 项目集(ProjectSet)是多个项目的统筹容器,不直接承载任务。
+- 任务(Task)必须归属项目,不直接归属产品。
+
+### 2.2 对项目管理的约束
+
+- 项目创建时必须选择所属产品。
+- 项目创建后不允许改挂产品。
+- 项目负责人和项目成员必须先属于所属产品的产品团队,才能被选入项目团队。
+- 当前产品上下文中的关联项目列表必须按项目权限二次过滤。
+
+### 2.3 对权限模型的约束
+
+- 产品团队权限和项目团队权限分离。
+- 产品可见不等于可编辑,不等于可在该产品下建项目。
+- 高风险动作和普通协作动作分离控制。
+- 删除、归档、暂停、恢复、废弃都需要独立动作权限和审计留痕。
+
+## 3. 功能架构
+
+### 3.1 功能拆分
+
+| 一级能力 | 二级能力 | V0.5 是否落地 | 说明 |
+|---|---|---|---|
+| 产品主数据 | 创建、编辑、查看、查询 | 是 | 主数据最小闭环 |
+| 产品团队 | 产品经理、产品成员、产品观察者管理 | 是 | 同时承接项目团队来源池 |
+| 产品生命周期 | 启用、暂停、归档、废弃、逻辑删除 | 是 | 删除为高风险动作 |
+| 产品与项目关系 | 项目创建时选择产品,产品侧查看关联项目 | 是 | 关系由项目模块维护 |
+| 产品需求 | 轻量需求池、基础状态流转、分流到项目 | 是 | 只承接通用需求 |
+| 产品版本 | 版本主数据、版本状态、目标版本 | 否 | 当前不包含 |
+| 产品路线图 | 时间线、版本计划 | 否 | 当前不包含 |
+
+### 3.2 页面信息架构
+
+```text
+产品管理
+├── 未选中产品时
+│ └── 产品对象入口页
+│ ├── 产品分组/分类
+│ ├── 产品列表
+│ └── 新建产品弹窗
+└── 已选中产品时
+ ├── 当前产品概览
+ ├── 产品需求
+ │ ├── 待确认需求
+ │ └── 产品需求维护弹窗
+ ├── 产品团队
+ └── 关联项目
+```
+
+## 4. 页面详细设计
+
+### 4.1 页面模型与布局原则
+
+产品管理采用“对象上下文型业务域”布局,不采用传统后台中“列表页 -> 详情页 -> 多页签详情”的固定写法。
+
+页面结构应分成两种状态:
+
+- 未选中产品时:头部只显示业务域锚点 `产品管理`,内容区承接产品对象入口页。
+- 已选中产品时:头部显示 `产品管理 + 当前产品`,并切换为该产品上下文下的功能导航。
+
+这意味着产品管理的核心不是单独设计多少个静态页面,而是先建立“当前是否已经进入某个产品上下文”的状态。
+
+V0.5 约束如下:
+
+- 未选中产品前,不在头部伪造一排产品功能菜单。
+- 已选中产品后,头部才显示当前产品下的功能导航。
+- 当前产品上下文内的功能项按 V0.5 已落地能力收敛,不预设额外功能带。
+- 产品新建、编辑统一采用弹窗,不拆成独立路由页;轻量需求新增和编辑可在当前产品上下文内通过弹窗完成。
+
+### 4.2 未选中产品时的入口态
+
+#### 4.2.1 头部状态
+
+- 显示业务域锚点:`产品管理`
+- 不显示当前产品对象位
+- 不显示产品上下文功能导航
+
+该状态的核心目的,是让用户先完成“选择哪个产品”这一步,而不是先进入产品下的某个功能页。
+
+#### 4.2.2 内容区结构
+
+内容区展示产品对象入口页,主要承载:
+
+- 产品分类或产品分组入口
+- 产品列表
+- 搜索、筛选
+- 新建产品入口
+
+产品分组入口放在内容区左侧的对象列表导航内,而不是放在头部。
+
+#### 4.2.3 查询与列表
+
+支持以下筛选条件:
+
+- 关键词:匹配产品编码、产品名称
+- 产品方向
+- 产品经理
+- 状态:启用、暂停、归档、废弃
+- 最近更新时间
+
+展示以下列表字段:
+
+- 产品编码
+- 产品方向
+- 产品名称
+- 产品经理
+- 状态
+- 关联项目数
+- 轻量需求数
+- 更新时间
+
+说明:
+
+- `关联项目数` 仅统计当前用户有权查看的项目数量。
+- `轻量需求数` 为产品下未删除需求数量。
+- 创建人、创建时间等审计字段不在入口态列表中展开,统一放在更多列或对象概览中查看。
+
+#### 4.2.4 入口态操作
+
+入口态只保留以下高频动作:
+
+- 新建产品
+- 进入产品
+- 编辑产品
+- 暂停 / 恢复 / 归档 / 废弃 / 删除
+
+规则如下:
+
+- `新建产品` 为独立主按钮,仅授权管理员和具备全局创建权限的产品经理可见。
+- 点击产品名称或行主操作,表示“进入该产品上下文”,而不是进入传统详情页。
+- 编辑和状态动作可通过行内更多菜单或右侧操作区触发,避免列表区堆积过多按钮。
+
+### 4.3 已选中产品时的对象上下文态
+
+#### 4.3.1 头部状态
+
+当用户从入口态选择某个产品后,系统建立当前产品上下文,头部应切换为:
+
+- 业务域锚点:`产品管理`
+- 当前对象位:当前产品名称
+- 功能导航区:显示当前产品上下文下的功能入口
+
+交互要求:
+
+- 点击业务域锚点,退出当前产品上下文并返回产品入口页。
+- 点击当前产品对象位,可打开产品切换面板,或回到产品列表重新选择。
+
+#### 4.3.2 V0.5 功能导航范围
+
+V0.5 下,当前产品上下文内的功能导航保持最小集合,仅承接以下内容:
+
+- 概览
+- 产品需求
+- 产品团队
+- 关联项目
+
+说明:
+
+- `概览` 承接产品基本信息、状态信息、关键摘要和常用动作。
+- `产品需求` 承接轻量需求池的查询和维护。
+- `产品团队` 承接产品团队成员管理。
+- `关联项目` 承接当前产品下项目查看。
+- `产品研发令号` 不作为 V0.5 头部导航项,通过独立入口承接。
+- `操作记录` 不作为 V0.5 头部一级功能导航单独占位,统一作为概览中的区块或独立记录查看页承接。
+
+#### 4.3.3 对象上下文保持规则
+
+- 用户进入某个产品后,应保持当前产品上下文,直到主动切换产品或返回产品入口页。
+- 用户在产品上下文内切换功能时,不应丢失当前产品。
+- 从头部切换产品时,优先保留当前功能;若目标产品下该功能不可用,则退回目标产品默认首页。
+
+### 4.4 当前产品上下文下的功能承载
+
+#### 4.4.1 概览
+
+`概览` 是当前产品上下文的默认首页,用于集中展示该产品的核心信息,而不是再拆成独立详情页。
+
+承载以下内容:
+
+- 产品编码、产品方向、产品名称、产品经理、状态、描述
+- 创建人、创建时间、更新时间等审计摘要
+- 关联项目数、轻量需求数、待确认需求数等统计摘要
+- 编辑、暂停、恢复、归档、废弃、删除等常用动作入口
+- 待确认需求区块,用于提示由工单流转到当前产品、但尚未被产品负责人认领或拒绝的需求
+- 最近动态区块,用于展示该产品最近发生的关键变更
+
+待确认需求展示:
+
+- 需求标题
+- 来源工单
+- 提交时间
+- 当前工单负责人
+- 快捷处理入口
+
+待确认需求支持:
+
+- 认领
+- 拒绝
+- 查看来源工单
+
+待确认需求规则:
+
+- 工单被归属到某产品后,不等于自动进入正式产品需求池。
+- 工单流转到产品侧后,应先进入该产品下的待确认需求队列。
+- 由产品负责人或授权管理员执行认领或拒绝。
+- 认领后,该需求进入正式产品需求池,状态进入 `待处理`。
+- 拒绝时必须填写原因,并保留与来源工单的追溯关系。
+- 本设计不承接工单状态回写;产品侧只承接来源追溯、认领拒绝结果和审计留痕。
+
+最近动态展示:
+
+- 产品创建
+- 产品编辑
+- 产品经理变更
+- 产品状态变更,如暂停、恢复、归档、废弃
+- 产品团队关键调整
+- 产品需求新增、关闭和关键状态流转
+
+最近动态展示字段:
+
+- 时间
+- 动作描述
+- 操作人
+- 原因或摘要说明
+
+最近动态的边界要求:
+
+- 仅展示最近 `10` 条关键记录,作为概览摘要信息
+- 数据来源以 `rdms_biz_audit_log` 和 `rdms_product_status_log` 为主;其中产品暂停/恢复/归档/废弃/删除来自 `rdms_product_status_log`,产品经理变更、团队成员调整、需求认领/拒绝/关闭等来自 `rdms_biz_audit_log`
+- 概览中的最近动态不替代完整审计记录查询
+- 完整留痕通过“查看更多操作记录”进入独立记录查看界面或展开面板
+
+规则:
+
+- `编辑` 仅授权管理员和当前产品经理可见。
+- `暂停` 仅启用状态可见。
+- `恢复` 仅暂停状态可见。
+- `归档` 仅启用、暂停状态可见。
+- `废弃` 仅启用、暂停状态可见。
+- `删除` 仅授权管理员可见,且保持高风险动作的二次确认要求。
+
+#### 4.4.2 产品需求
+
+`产品需求` 承接当前产品下的轻量需求池管理,是 V0.5 产品上下文内最核心的功能页之一。
+
+支持以下筛选条件:
+
+- 关键词
+- 分类
+- 来源
+- 状态
+- 优先级
+- 提出人
+- 默认实现项目
+- 是否待确认
+
+展示:
+
+- 需求标题
+- 分类
+- 来源
+- 来源引用信息
+- 优先级
+- 状态
+- 提出人
+- 默认实现项目
+- 更新时间
+
+支持:
+
+- 新增需求
+- 查看需求
+- 编辑需求
+- 认领工单流转需求
+- 拒绝工单流转需求
+- 状态流转
+- 关闭需求
+
+规则:
+
+- 产品需求来源至少包括手工新增和工单流转两类。
+- 对于工单流转到产品侧的需求,需先由产品负责人或授权管理员认领或拒绝;认领后才进入正式产品需求池。
+- 来源字段应支持区分 `手工新增`、`工单流转` 等来源类型;如来源于工单,还应展示来源工单编号或来源引用信息。
+- 产品需求列表应支持按来源筛选,便于单独查看由工单流转进入产品侧的需求。
+- 即使工单流转到产品侧的需求最终被拒绝,也应继续保留在产品需求列表中,并允许按来源和状态查询追溯,不应因被拒绝而从产品侧视图中消失。
+- 产品需求既支持来源追溯,也支持父子拆分;来源追溯和拆分关系分开建模。
+- 同一产品下,同一来源工单只允许沉淀 1 条源头需求记录;该源头需求和手工新增需求都允许继续拆分为 N 条子需求。
+- 子需求不参与来源唯一约束;拆分链路统一由 `parent_requirement_id`、`root_requirement_id` 承接。
+- 录入和维护默认仅产品经理、授权管理员可执行。
+- 产品成员、产品观察者默认仅查看。
+- 已关闭、已拒绝、已取消需求默认只读。
+- V0.5 的 `产品需求` 只承接轻量需求池管理;是否进入 `待评审` 由业务判断决定,但不在当前版本展开正式审批流程、工作流节点和复杂评审编排。
+
+#### 4.4.3 产品团队
+
+`产品团队` 承接当前产品下的成员关系管理。
+
+展示:
+
+- 用户姓名
+- 团队角色
+- 状态
+- 加入时间
+- 退出时间
+
+支持:
+
+- 新增成员
+- 调整角色
+- 移出产品团队
+
+规则:
+
+- 同一产品下,同一用户只允许一条有效团队关系。
+- 同一时刻只允许一个有效产品经理。
+- 将某成员设为产品经理时,需要同步更新产品主表的 `manager_user_id`。
+- 新增成员、调整角色、移出产品团队、变更产品经理等非状态敏感动作统一写入 `rdms_biz_audit_log`。
+- 产品团队成员退出后,不自动影响其已参与项目的历史关系。
+- 已退出产品团队的成员,不能再被该产品下的新项目继续选入。
+
+#### 4.4.4 关联项目
+
+`关联项目` 承接当前产品下项目的查看,不承接项目归属维护。
+
+展示:
+
+- 项目编码
+- 项目名称
+- 项目负责人
+- 项目状态
+- 更新时间
+
+规则:
+
+- 只展示当前用户有项目查看权限的数据。
+- 不提供项目改挂产品入口。
+- 产品经理不因负责产品而自动获得该产品下全部项目查看或编辑权限。
+
+### 4.5 对象维护方式
+
+#### 4.5.1 产品新建与编辑
+
+产品新建和编辑作为对象维护动作处理,不单独定义独立页面。
+
+承接方式:
+
+- 新建产品:在入口态通过弹窗完成
+- 编辑产品:在当前产品上下文的概览中触发弹窗完成
+
+表单字段保持一致:
+
+- 产品编码
+- 产品方向
+- 产品名称
+- 产品经理
+- 产品描述
+
+规则:
+
+- 产品编码支持手动录入或自动生成;创建后不可修改。
+- 产品名称需校验未删除范围唯一。
+- 产品方向为必填正式字段,统一通过系统字典承接;字典类型编码统一为 `rdms_product_direction`,业务表字段 `direction_code` 存字典 `value`,不存字典数据主键 ID。
+- 研发令号不作为产品主表字段维护,按年度通过独立的产品研发令号能力维护。
+- 产品经理允许调整,不强制等于创建人。
+- 若产品经理不等于创建人,创建人只保留审计意义,不自动获得持续管理权限。
+- 保存成功后自动写入创建、更新审计字段;创建时默认状态编码为 `active`,并自动建立产品经理团队关系。
+
+#### 4.5.2 轻量需求维护
+
+轻量需求的新增和编辑,通过当前产品上下文内的弹窗完成,不拆为独立页面。
+
+表单字段包括:
+
+- 需求标题
+- 需求分类
+- 需求来源
+- 来源引用信息
+- 优先级
+- 需求描述
+- 提出人
+- 默认实现项目
+
+规则:
+
+- 产品需求来源至少包括手工新增和工单流转两类。
+- 手工新增需求通过新增弹窗录入,默认状态编码为 `pending_process`,显示状态为 `待处理`。
+- 工单流转到产品侧的需求,不通过普通新增弹窗直接进入正式需求池,而是先进入待确认队列,由产品负责人或授权管理员认领或拒绝。
+- 认领后,该需求进入正式产品需求池,状态进入 `待处理`;拒绝时必须填写原因,并保留来源工单追溯关系。
+- 工单来源需求在同一产品下只允许形成 1 条源头需求记录;后续如需细化,统一从该源头需求继续拆分子需求。
+- 手工新增需求也允许继续拆分子需求;来源追溯和拆分关系分开承接。
+- 是否进入 `待评审` 由业务判断决定;需要评审的需求进入 `待评审`,不需要评审的需求直接进入 `待分流`。
+- 一个产品需求只能归属一个产品。
+- `需求来源` 用于标识来源类型,如 `手工新增`、`工单流转`;如来源于工单,还应在 `来源引用信息` 中展示来源工单编号或引用信息。
+- `默认实现项目` 如有值,必须指向当前产品下的合法项目。
+- `reject`、`cancel`、`close` 等终态动作统一回写主表 `terminal_action_code`、`terminal_reason`、`terminal_time`。
+- V0.5 不启用目标版本字段。
+
+## 5. 对象关系与数据设计
+
+本章描述数据关系与承接口径,物理表结构以专项 SQL 文档为准。
+
+### 5.1 数据结构承接口径
+
+- 产品管理实际执行 SQL 统一以 `rdms-project/rdms-project-boot/src/main/resources/sql/product/01_product_schema.sql` 为准;专项 SQL 文档仅承担设计审阅和说明。
+- 系统 RBAC 角色与资源元数据以 `04-设计阶段/数据库/system_menu.sql` 的 DDL 为准。
+- RDMS 对象成员关系统一由 `rdms_user_object_role` 承接。
+- 产品研发令号统一由 `rdms_product_rd_order` 承接,按产品年度维护;同一产品可维护多个年度研发令号。
+- 产品方向统一由系统字典承接;字典类型编码统一为 `rdms_product_direction`,`rdms_product.direction_code` 存字典 `value`,不存 `system_dict_data.id`。
+- 产品状态统一由 `rdms_object_status_model`、`rdms_object_status_transition` 承接;`rdms_product.status_code` 存状态编码。
+- 产品需求统一由 `rdms_product_requirement` 承接;`status_code` 存需求状态编码,状态定义与流转统一由 `rdms_object_status_model`、`rdms_object_status_transition` 承接,其中 `object_type = product_requirement`。
+- 产品需求来源追溯和拆分关系分开建模;`source_type`、`source_biz_type`、`source_biz_id`、`source_biz_code` 承接来源追溯,`parent_requirement_id`、`root_requirement_id` 承接拆分链路。
+- 同一产品下,同一来源工单只允许一条源头需求记录;源头需求和手工新增需求都允许继续拆分为多个子需求,子需求不参与来源唯一约束。
+- 产品需求主表统一承接终态结果字段 `terminal_action_code`、`terminal_reason`、`terminal_time`;完整过程留痕继续由 `rdms_biz_audit_log` 承接。
+- 产品状态留痕必须保留 `from_status`、`to_status` 字段;产品状态日志和通用业务审计日志统一按状态编码记录前后变化。
+
+### 5.2 保留的核心关系
+
+```text
+产品(Product) 1 ---- N 项目(Project)
+ |
+ +---- N RDMS对象成员关系(UserObjectRole, object_type=product)
+ |
+ +---- N 产品研发令号(ProductRdOrder)
+ |
+ +---- N 产品需求(ProductRequirement)
+ |
+ +---- N 子需求(ProductRequirement, parent_requirement_id)
+```
+
+关系约束:
+
+- 每个项目必须归属一个产品。
+- 产品与项目关系由项目模块的 `product_id` 维护,不建立独立中间表。
+- 产品团队和项目团队不是同一对象;项目团队是产品团队中的执行子集。
+- 产品研发令号不属于产品主数据的一对一字段;按产品维度一对多维护。
+- 同一产品同一年度只允许一个有效研发令号;研发令号本身默认不做全局唯一约束。
+- 产品需求只归属产品,不直接归属项目;需要项目承接时通过 `implement_project_id` 建立默认实现关系。
+- 来源追溯统一挂在源头需求上,父子拆分通过 `parent_requirement_id`、`root_requirement_id` 承接,不混用同一组字段表达两类关系。
+- 同一产品下,同一来源工单只保留 1 条源头需求记录;该源头需求和手工新增需求都可继续拆分子需求。
+- 当前用户在某产品中的对象角色,由 `rdms_user_object_role` 绑定 `system_role` 中 `scope_type = object`、`object_type = product` 的角色定义。
+- 当前产品上下文下的导航和按钮,由 `system_role`、`system_menu`、`system_role_menu` 共同驱动。
+
+## 6. 详细业务规则
+
+### 6.1 主数据规则
+
+| 编号 | 规则 |
+|---:|---|
+| PDD-01 | 创建产品时必须填写产品名称、产品方向、产品经理;产品编码可手动录入,也可由系统自动生成。 |
+| PDD-02 | 产品编码自动生成格式统一为 `CNPD + YYYY + NNN`,按年从 `001` 递增。 |
+| PDD-03 | 产品编码、产品名称在未删除范围内必须唯一。 |
+| PDD-04 | 产品创建后默认状态编码为 `active`,对应产品状态模型中的“启用”。 |
+| PDD-05 | 产品编码创建后不可修改。 |
+| PDD-06 | 产品研发令号不作为产品主表字段;应按产品维度一对多维护,统一由 `rdms_product_rd_order` 承接。 |
+| PDD-07 | 同一产品同一年度只允许一个有效研发令号;研发令号本身默认不做全局唯一约束。 |
+| PDD-08 | 产品方向为必填正式字段,统一通过系统字典承接;字典类型编码统一为 `rdms_product_direction`,`direction_code` 存字典 `value`,初始值为 `embedded`、`power_electronics`、`system_group`。 |
+| PDD-09 | 产品经理允许变更;创建人不可编辑修改。 |
+| PDD-10 | 产品不引入产品模板、产品类型等其他正式扩展字段;方向字段除外。 |
+
+### 6.2 产品团队规则
+
+| 编号 | 规则 |
+|---:|---|
+| PDD-10 | V0.5 产品团队默认提供 `产品经理`、`产品成员`、`产品观察者` 三类对象角色。 |
+| PDD-11 | 同一产品下,同一用户只允许存在一条有效团队关系。 |
+| PDD-12 | 同一产品下,同一时刻只允许一个有效产品经理。 |
+| PDD-13 | 产品经理变更时,必须同步产品主表和团队关系。 |
+| PDD-14 | 产品团队成员是项目团队的人员来源池,但不自动继承项目权限。 |
+| PDD-15 | 已退出产品团队的成员不影响历史项目关系,但不能继续被该产品下的新项目选入。 |
+
+### 6.3 产品与项目关系规则
+
+| 编号 | 规则 |
+|---:|---|
+| PDD-20 | 项目创建时必须选择所属产品。 |
+| PDD-21 | 项目创建后不允许改挂产品。 |
+| PDD-22 | 当前产品上下文中的关联项目列表必须按项目权限二次过滤。 |
+| PDD-23 | 建项目时的产品选择范围,按“`status_code = active` + 当前用户具备该产品建项目资格”过滤。 |
+| PDD-24 | V0.5 默认仅产品经理和授权管理员具备在产品下建项目资格。 |
+
+### 6.4 产品可见范围规则
+
+统一口径:
+
+`产品可见范围 = 关联项目反推范围 + 产品团队范围 + 创建人范围 + 授权管理员范围 + 特殊授权范围`
+
+细化规则:
+
+| 编号 | 规则 |
+|---:|---|
+| PDD-30 | 已有关联项目的产品,任何可见该产品下至少一个项目的用户,均可查看产品基础信息。 |
+| PDD-31 | 尚未关联项目的产品,仅产品团队、创建人、授权管理员和特殊授权用户可见。 |
+| PDD-32 | 产品可见不等于可编辑,也不等于可在该产品下建项目。 |
+| PDD-33 | 关联项目、任务、工时、周报等执行数据权限仍按项目和任务侧口径控制。 |
+
+### 6.5 轻量需求规则
+
+| 编号 | 规则 |
+|---:|---|
+| PDD-40 | 产品需求只承接通用、可复用、应进入标准产品能力的需求。 |
+| PDD-41 | 单一项目、单一区域、单一客户交付诉求,应进入项目需求,不进入产品需求池。 |
+| PDD-42 | 手工新增的产品需求默认进入 `待处理`;工单流转到产品侧的需求默认先进入 `待确认`。 |
+| PDD-43 | 产品需求最多只挂一个默认实现项目。 |
+| PDD-44 | 默认实现项目如有值,必须属于当前产品。 |
+| PDD-45 | 产品需求录入、认领、拒绝、维护、关闭默认由产品经理或授权管理员承接。 |
+| PDD-46 | 是否进入 `待评审` 由业务判断决定;需要评审的需求进入 `待评审`,不需要评审的需求可直接从 `待处理` 进入 `待分流`。 |
+| PDD-47 | V0.5 不引入正式审批流、工作流引擎和复杂评审编排;`待评审` 仅表达业务评审待处理状态。 |
+| PDD-48 | V0.5 不启用目标版本字段。 |
+| PDD-49 | 产品需求本身不承接排期状态;排期由项目侧承接。 |
+| PDD-50 | 工单流转到产品侧后,不等于自动进入正式产品需求池,需由产品负责人或授权管理员认领或拒绝。 |
+| PDD-51 | 产品需求应保留来源类型和来源业务追溯信息;被拒绝的工单流转需求仍需可按来源与状态查询追溯。 |
+| PDD-52 | 产品需求主表统一使用 `status_code`。 |
+| PDD-53 | 同一产品下,同一来源工单只允许沉淀 1 条源头需求记录;该源头需求和手工新增需求都可继续拆分为 N 条子需求。 |
+| PDD-54 | 来源追溯和拆分关系分开建模;来源由 `source_*` 字段承接,拆分由 `parent_requirement_id`、`root_requirement_id` 承接。 |
+| PDD-55 | 子需求不参与来源唯一约束。 |
+| PDD-56 | `reject`、`cancel`、`close` 等终态动作统一回写 `terminal_action_code`、`terminal_reason`、`terminal_time`,审计日志保留完整过程留痕。 |
+
+## 7. 状态机与流程设计
+
+### 7.1 产品状态定义
+
+| 状态 | 说明 | 是否可被新建项目选择 | 是否允许编辑 | 是否允许新增需求 |
+|---|---|---|---|---|
+| 启用(`active`) | 正常可用 | 是 | 是 | 是 |
+| 暂停(`paused`) | 受环境或资源限制临时暂停推进,仅保留交接与存量维护 | 否 | 是,仅限交接与存量维护 | 否 |
+| 归档(`archived`) | 历史留存,只读为主 | 否 | 否 | 否 |
+| 废弃(`abandoned`) | 经调研或决策确认不再继续推进 | 否 | 否 | 否 |
+| 逻辑删除 | 删除结果 | 否 | 否 | 否 |
+
+说明:
+
+- `rdms_product` 主表统一存 `status_code`。
+- 产品状态定义统一由 `rdms_object_status_model` 承接;状态动作与流转统一由 `rdms_object_status_transition` 承接。
+- `逻辑删除` 不纳入正常状态模型,通过 `deleted` 表达。
+- `paused` 允许通过 `resume` 动作恢复到 `active`。
+- `paused` 后不自动解绑历史项目和历史需求关系。
+- `paused` 后仅允许产品经理交接、产品团队维护、描述/备注修正和恢复启用,不允许继续新增产品需求。
+- `archived`、`abandoned` 和删除在 V0.5 不支持恢复。
+
+### 7.2 产品状态流转
+
+允许流转:
+
+- `active` --`pause`--> `paused`
+- `paused` --`resume`--> `active`
+- `active` --`archive`--> `archived`
+- `paused` --`archive`--> `archived`
+- `active` --`abandon`--> `abandoned`
+- `paused` --`abandon`--> `abandoned`
+
+补充说明:
+
+- 上述流转以 `rdms_object_status_transition` 中 `object_type = product` 的配置为准。
+- 逻辑删除不属于正常状态流转,不写入 `to_status_code` 终态,而是通过 `deleted = 1` 表达。
+
+### 7.3 产品状态动作前置条件
+
+#### 7.3.1 暂停(`pause`)
+
+前置条件:
+
+- 当前状态为 `active`。
+- 产品下不存在状态为 `待开始`、`进行中`、`已暂停` 的项目。
+- 产品下不存在阻塞暂停的执行和任务。
+- 操作人具备暂停权限。
+
+动作要求:
+
+- 必填暂停原因。
+- 暂停后不自动解绑历史项目和历史需求关系。
+- 暂停后仅允许产品经理交接、产品团队维护、描述/备注修正和恢复启用。
+- 暂停后不允许新增产品需求;已有需求可继续按权限查看、认领、分流、关闭等存量维护动作。
+- 写入状态日志。
+
+#### 7.3.2 恢复(`resume`)
+
+前置条件:
+
+- 当前状态为 `paused`。
+- 操作人具备恢复权限。
+
+动作要求:
+
+- 不强制填写原因,允许补充说明。
+- 写入状态日志。
+
+#### 7.3.3 归档(`archive`)
+
+前置条件:
+
+- 当前状态为 `active` 或 `paused`。
+- 产品下不存在状态为 `待开始`、`进行中`、`已暂停` 的项目。
+- 产品下不存在阻塞归档的执行和任务。
+- 操作人具备归档权限。
+
+动作要求:
+
+- 必填归档原因。
+- 归档后产品默认只读。
+- 写入状态日志。
+
+#### 7.3.4 废弃(`abandon`)
+
+前置条件:
+
+- 当前状态为 `active` 或 `paused`。
+- 产品下不存在状态为 `待开始`、`进行中`、`已暂停` 的项目。
+- 产品下不存在阻塞废弃的执行和任务。
+- 操作人具备废弃权限。
+
+动作要求:
+
+- 必填废弃原因。
+- 废弃后产品默认只读。
+- 废弃后不支持恢复。
+- 写入状态日志。
+
+#### 7.3.5 逻辑删除
+
+前置条件:
+
+- 产品下关联项目均已满足项目侧删除条件并已逻辑删除。
+- 操作人具备删除权限。
+- 二次身份确认通过。
+- 二次输入产品名称确认通过。
+
+动作要求:
+
+- 必填删除原因。
+- 写入删除人、删除时间、删除原因。
+- 写入状态日志。
+
+### 7.4 产品需求状态定义
+
+| 状态编码 | 状态名称 | 说明 |
+|---|---|---|
+| `pending_confirm` | 待确认 | 工单流转到产品侧后的待确认状态,待产品负责人认领或拒绝 |
+| `pending_process` | 待处理 | 手工新增后的默认状态,或工单流转需求认领后的正式初始状态 |
+| `pending_review` | 待评审 | 仅针对需要评审的需求,待产品侧完成业务评审判断 |
+| `pending_dispatch` | 待分流 | 需求成立,等待明确承接方向 |
+| `dispatched` | 已分流 | 已明确承接方向 |
+| `in_progress` | 实施中 | 承接项目已进入正式执行 |
+| `accepted` | 已验收 | 承接结果已完成验收 |
+| `closed` | 已关闭 | 生命周期闭环完成 |
+| `rejected` | 已拒绝 | 评审后确认不成立 |
+| `canceled` | 已取消 | 原已进入推进链路后终止 |
+
+### 7.5 产品需求状态流转
+
+允许流转:
+
+- 待确认 -> 待处理
+- 待确认 -> 已拒绝
+- 待确认 -> 已取消
+- 待处理 -> 待评审
+- 待处理 -> 待分流
+- 待处理 -> 已拒绝
+- 待处理 -> 已取消
+- 待评审 -> 待分流
+- 待评审 -> 已拒绝
+- 待评审 -> 已取消
+- 待分流 -> 已分流
+- 待分流 -> 已取消
+- 已分流 -> 实施中
+- 已分流 -> 已取消
+- 实施中 -> 已验收
+- 实施中 -> 已取消
+- 已验收 -> 已关闭
+
+补充规则:
+
+- 产品需求主表统一存 `status_code`;状态定义与流转统一以 `rdms_object_status_model`、`rdms_object_status_transition` 中 `object_type = product_requirement` 的配置为准。
+- 手工新增需求首状态为 `待处理`;工单流转需求首状态为 `待确认`。
+- `待确认 -> 待处理` 表示产品负责人已认领,需求正式进入产品需求池。
+- 只有需要评审的需求才进入 `待评审`;不需要评审的需求可直接从 `待处理` 进入 `待分流`。
+- `待评审` 仅表达业务评审待处理状态,V0.5 不展开正式审批流、会签/或签或工作流引擎实现。
+- `已分流 -> 实施中` 以承接项目进入正式执行为触发条件。
+- 到达 `已拒绝`、`已取消`、`已关闭` 时,主表同步回写 `terminal_action_code`、`terminal_reason`、`terminal_time`。
+- 产品需求认领、拒绝、分流、关闭等关键动作统一写入 `rdms_biz_audit_log`。
+- 已关闭、已拒绝、已取消状态默认不再继续流转。
+
+### 7.6 关键流程
+
+#### 7.6.1 创建产品
+
+1. 用户在产品入口态点击“新建产品”,打开产品新增弹窗。
+2. 填写产品名称、产品方向、产品经理、产品描述,可手动录入产品编码。
+3. 系统校验名称唯一、编码唯一、产品方向字典值合法、产品经理有效。
+4. 编码为空时系统自动生成编码。
+5. 保存产品主数据,默认状态编码设为 `active`。
+6. 自动建立产品经理团队关系。
+7. 写入创建审计记录。
+
+#### 7.6.2 变更产品经理
+
+1. 当前产品经理或授权管理员发起编辑。
+2. 选择新的产品经理。
+3. 系统校验目标用户有效。
+4. 更新 `manager_user_id`。
+5. 同步调整产品团队角色关系,保证仅一个有效 `manager`。
+6. 写入 `rdms_biz_audit_log`,记录变更前后产品经理、操作者和原因。
+
+#### 7.6.3 暂停产品
+
+1. 用户发起 `pause` 动作。
+2. 系统按 `rdms_object_status_transition` 校验 `active -> paused` 流转存在且可用。
+3. 系统检查项目、执行、任务阻塞项。
+4. 用户填写暂停原因。
+5. 校验通过后写入状态编码为 `paused`。
+6. 写入 `rdms_product_status_log`。
+
+#### 7.6.4 恢复产品
+
+1. 用户发起 `resume` 动作。
+2. 系统按 `rdms_object_status_transition` 校验 `paused -> active` 流转存在且可用。
+3. 校验通过后写入状态编码为 `active`。
+4. 写入 `rdms_product_status_log`。
+
+#### 7.6.5 归档产品
+
+1. 用户发起 `archive` 动作。
+2. 系统按 `rdms_object_status_transition` 校验目标流转存在且可用。
+3. 系统检查归档阻塞项。
+4. 用户填写归档原因。
+5. 校验通过后更新为 `archived`。
+6. 写入 `rdms_product_status_log`。
+
+#### 7.6.6 废弃产品
+
+1. 用户发起 `abandon` 动作。
+2. 系统按 `rdms_object_status_transition` 校验目标流转存在且可用。
+3. 系统检查废弃阻塞项。
+4. 用户填写废弃原因。
+5. 校验通过后更新为 `abandoned`。
+6. 写入 `rdms_product_status_log`。
+
+#### 7.6.7 删除产品
+
+1. 授权管理员发起删除动作。
+2. 系统复核关联项目是否已全部逻辑删除。
+3. 弹出二次身份确认。
+4. 要求输入产品名称确认。
+5. 用户填写删除原因。
+6. 校验通过后执行逻辑删除,并写入 `rdms_product_status_log`。
+
+#### 7.6.8 新增产品需求
+
+1. 产品经理或授权管理员在当前产品上下文的“产品需求”功能中点击“新增需求”。
+2. 系统弹出新增需求弹窗,填写需求标题、分类、来源、优先级、描述、提出人、默认实现项目。
+3. 系统校验默认实现项目属于当前产品。
+4. 保存后状态编码默认为 `pending_process`,显示状态为 `待处理`。
+5. 写入 `rdms_biz_audit_log`。
+
+#### 7.6.9 认领或拒绝工单流转需求
+
+1. 工单归属到某产品后,产品侧生成一条来源为工单流转的源头需求记录,状态编码为 `pending_confirm`。
+2. 产品负责人或授权管理员在概览的待确认需求区块,或在产品需求列表的待确认视图中查看该记录。
+3. 若认领,系统将需求状态从 `pending_confirm` 更新为 `pending_process`,并保留来源工单追溯信息。
+4. 若拒绝,系统要求填写拒绝原因,并将需求状态更新为 `已拒绝`。
+5. 认领或拒绝后,系统写入 `rdms_biz_audit_log`;本设计不承接工单状态回写。
+
+#### 7.6.10 产品需求分流
+
+1. 产品经理在产品需求中判断该诉求是否成立。
+2. 若不成立,转 `已拒绝`。
+3. 若成立但待确定承接方向,转 `待分流`。
+4. 若已明确由具体项目承接,写入默认实现项目并转 `已分流`。
+5. 本次分流动作写入 `rdms_biz_audit_log`。
+6. 项目侧承接后,按项目需求和执行流转规则继续推进。
+
+## 8. 权限与动作矩阵
+
+### 8.1 角色定义
+
+| 角色 | 说明 |
+|---|---|
+| 授权管理员 | 全局管理和高风险动作控制者 |
+| 产品经理 | 单个产品的核心责任人 |
+| 产品成员 | 产品侧协作成员 |
+| 产品观察者 | 产品侧只读协作成员 |
+| 创建人 | 审计身份,不作为长期业务角色 |
+
+### 8.2 动作矩阵
+
+| 动作 | 授权管理员 | 产品经理 | 产品成员 | 产品观察者 | 创建人(非产品经理) |
+|---|---|---|---|---|---|
+| 查看产品 | 是 | 是 | 是 | 是 | 是 |
+| 创建产品 | 是 | 有条件 | 否 | 否 | 否 |
+| 编辑产品 | 是 | 是 | 否 | 否 | 否 |
+| 管理产品团队 | 是 | 是 | 否 | 否 | 否 |
+| 暂停/恢复/归档/废弃 | 是 | 是 | 否 | 否 | 否 |
+| 删除产品 | 是 | 否 | 否 | 否 | 否 |
+| 查看轻量需求 | 是 | 是 | 是 | 是 | 是 |
+| 新增轻量需求 | 是 | 是 | 否 | 否 | 否 |
+| 编辑轻量需求 | 是 | 是 | 否 | 否 | 否 |
+| 关闭轻量需求 | 是 | 是 | 否 | 否 | 否 |
+| 在该产品下建项目 | 是 | 是 | 否 | 否 | 否 |
+
+说明:
+
+- “有条件”表示需要同时具备全局创建产品权限。
+- 创建人若不是产品经理,只保留查看自己创建产品的可见补充,不自动拥有编辑和管理权限。
+
+### 8.3 对象上下文权限与导航控制
+
+产品管理属于对象上下文型业务域,因此权限判断需要拆成两层:
+
+- 全局 RBAC:控制用户能否进入 `产品管理` 业务域
+- 对象上下文 RBAC:控制用户在选中某个产品后,能看到哪些功能导航、访问哪些页面、使用哪些按钮
+
+对象上下文下的权限控制规则如下:
+
+- 当前用户在某产品中的对象角色,以 `rdms_user_object_role.role_id` 为准,且 `object_type = product`。
+- 头部功能导航不直接按全局菜单固定展示,而应根据对象角色绑定的对象资源动态返回。
+- 页面按钮不直接复用全局按钮集合,而应根据当前产品上下文内的对象资源权限单独判断。
+
+V0.5 产品对象上下文按以下方式控制:
+
+- 产品经理:可见 `概览`、`产品需求`、`产品团队`、`关联项目`,并具备产品编辑、状态动作、需求维护、团队维护等按钮权限。
+- 产品成员:可见 `概览`、`产品需求`、`关联项目`,默认不具备产品状态动作和团队维护权限。
+- 产品观察者:可见 `概览`、`产品需求`、`关联项目`,默认只读。
+
+说明:
+
+- 是否展示 `产品团队` 导航项,应以对象角色授权结果为准,不要求所有产品角色都能看到。
+- 即使两个用户都能进入产品管理业务域,在不同产品中也可能看到不同的导航与按钮。
+- 导航、页面、按钮三类资源应统一通过对象资源模型配置,不在前端写死角色判断。
+
+## 9. 接口承接
+
+### 9.1 产品主数据接口
+
+- `GET /products`
+- `GET /products/{id}`
+- `GET /products/{id}/context`
+- `POST /products`
+- `PUT /products/{id}`
+- `POST /products/{id}/change-status`
+- `DELETE /products/{id}`
+
+### 9.2 产品团队接口
+
+- `GET /products/{id}/members`
+- `POST /products/{id}/members`
+- `PUT /products/{id}/members/{memberId}`
+- `POST /products/{id}/members/{memberId}/inactive`
+
+### 9.3 产品需求接口
+
+- `GET /products/{id}/requirements`
+- `POST /products/{id}/requirements`
+- `GET /products/{id}/requirements/{requirementId}`
+- `PUT /products/{id}/requirements/{requirementId}`
+- `POST /products/{id}/requirements/{requirementId}/change-status`
+
+### 9.4 产品关联与动态接口
+
+- `GET /products/{id}/projects`
+- `GET /products/{id}/activities`
+
+补充说明:
+
+- 概览中的待确认需求区块可直接复用 `GET /products/{id}/requirements`,并按 `statusCode=pending_confirm`、`sourceType=work_order`、`pageSize` 取前 `N` 条,无需重复建设独立接口。
+- `GET /products/{id}/projects` 只承接关联项目查看,列表结果仍需按项目权限二次过滤。
+- `GET /products/{id}/activities` 用于承接概览中的最近动态与“查看更多操作记录”,数据来源以 `rdms_biz_audit_log`、`rdms_product_status_log` 为主。
+
+### 9.5 依赖与选择器接口说明
+
+- 产品经理选择器复用用户中心用户查询接口 `GET /users`;产品服务负责校验目标用户有效性。
+- 产品团队成员选择器复用用户中心或组织用户查询接口 `GET /users`;产品服务负责校验成员有效性和产品团队约束。
+- 默认实现项目选择器复用项目模块查询接口 `GET /projects?productId={id}`;只返回当前产品下、当前用户可见的合法项目。
+- 查看来源工单复用工单模块详情接口 `GET /work-orders/{id}`;产品模块只保留来源引用信息。
+
+### 9.6 接口共性要求
+
+- 列表接口统一支持分页、筛选、排序。
+- 列表和详情接口必须做服务端权限裁剪。
+- 对象上下文型业务域应提供按对象返回上下文能力的接口,至少返回当前对象摘要、当前用户对象角色、对象导航集合、对象按钮权限集合和默认首页。
+- 状态动作不得复用通用编辑接口混写。
+- `POST /products/{id}/change-status` 必须按动作编码传参,不允许直接透传任意目标状态;V0.5 产品状态动作统一支持 `pause`、`resume`、`archive`、`abandon`。
+- `POST /products/{id}/requirements/{requirementId}/change-status` 必须按业务动作传参,不允许直接透传任意目标状态值;统一支持 `claim`、`to_review`、`to_dispatch`、`dispatch`、`reject`、`cancel`、`close` 动作。
+- `claim`、`reject` 动作仅适用于工单流转形成的待确认需求;`reject`、`cancel`、`close` 必须传原因。
+- `dispatch` 若直接明确承接项目,则必须同时传入 `implementProjectId`;服务端需校验该项目属于当前产品。
+- 需求详情和需求列表返回体中应保留 `statusCode`、`sourceType`、`sourceBizType`、`sourceBizId`、`sourceBizCode`、`parentRequirementId`、`rootRequirementId`、`terminalActionCode`、`terminalReason`、`terminalTime` 等字段,便于前端联动来源追溯、拆分链路和结果态展示。
+- 删除、归档、暂停、恢复、废弃、关闭等敏感动作必须记录审计信息。
+
+## 10. 异常与边界
+
+### 10.1 重复创建
+
+- 前端需要防重复提交。
+- 后端以唯一约束作为最终兜底。
+
+### 10.2 产品重名
+
+- 若新名称与未删除产品重名,则拒绝保存。
+
+### 10.3 产品建错
+
+- 不支持直接修改项目的 `product_id` 纠偏。
+- 统一按“关闭/作废旧项目 + 新建正确项目”处理。
+
+### 10.4 产品经理离岗
+
+- 用户停用前应先完成产品经理转交。
+- 系统应提示存在未交接产品,避免静默失效。
+
+### 10.5 无项目产品
+
+- 产品创建后允许暂时没有项目。
+- 无项目产品不应全员可见。
+- 仅产品团队、创建人、授权管理员和特殊授权用户可见。
+
+### 10.6 暂停、归档和废弃后的历史关系
+
+- 暂停、归档、废弃不自动解除历史项目归属。
+- 历史统计、历史审计保留对该产品的引用。
+- 暂停、归档、废弃影响的是新增和维护能力,不影响历史追溯。
+
+## 11. 测试关注点
+
+### 11.1 功能测试
+
+- 产品创建、编辑、查看、查询。
+- 产品编码手动录入与自动生成。
+- 产品方向必填、字典值校验、筛选与展示。
+- 产品研发令号按年度维护与同一产品同年度唯一校验。
+- 产品经理变更与团队同步。
+- 暂停、恢复、归档、废弃、删除动作。
+- 轻量需求创建、编辑、来源筛选、需求拆分、状态流转、关闭。
+- 工单流转需求的待确认、认领、拒绝与追溯。
+
+### 11.2 权限测试
+
+- 不同角色的按钮可见性。
+- 无权限用户越权编辑、删除、暂停、恢复、废弃拦截。
+- 当前产品上下文中的关联项目是否按项目权限二次过滤。
+- 建项目产品选择器是否按资格过滤。
+
+### 11.3 数据一致性测试
+
+- 产品经理字段与产品团队 `manager` 关系一致。
+- 删除前关联项目删除条件复核正确。
+- 默认实现项目必须属于当前产品。
+- 产品需求分流后与项目侧口径一致。
+- 工单流转需求的来源类型、来源业务 ID、来源业务编号是否保留一致。
+- 同一产品同一来源工单是否只生成 1 条源头需求记录,子需求是否不参与来源唯一约束。
+- 产品需求主表 `status_code`、`terminal_*` 字段与审计日志记录是否一致。
+
+### 11.4 回归测试
+
+- 产品暂停后是否不能再被新建项目选择。
+- 归档后产品是否只读。
+- 废弃后产品是否只读且不可恢复。
+- 创建人非产品经理时权限是否未被错误放大。
+- 产品成员退出后是否不再进入新项目成员池。
+
+## 12. 对下游文档的约束
+
+### 12.1 对数据库设计文档
+
+- 产品管理相关物理表结构统一由产品管理专项 SQL 文档承接。
+- 必须落正式表:`rdms_product`、`rdms_product_rd_order`、`rdms_user_object_role`、`rdms_product_requirement`、`rdms_product_status_log`、`rdms_biz_audit_log`。
+- 必须落正式表:`rdms_object_status_model`、`rdms_object_status_transition`,并写入产品和产品需求的状态定义与流转配置。
+- 对象上下文 RBAC 的角色与资源元数据统一复用 `system_role`、`system_menu`、`system_role_menu`,并通过 DDL 定义 `scope_type`、`object_type` 字段。
+- `rdms_biz_audit_log` 虽放在产品管理专项 SQL 中维护,但定位为 RDMS 通用业务审计表,产品、项目共用。
+- 必须实现产品编码、产品名称的未删除范围唯一约束,以及产品研发令号按产品年度的未删除范围唯一约束。
+- 产品方向必须统一通过系统字典承接;字典类型编码统一为 `rdms_product_direction`,业务表字段存字典 `value`,不存字典数据主键 ID。
+- `rdms_product_requirement` 主表必须使用 `status_code`。
+- 必须区分来源追溯字段和需求拆分字段,不得再用同一组字段混合承接两类关系。
+- 必须保留 `terminal_action_code`、`terminal_reason`、`terminal_time` 三个终态结果字段。
+- 必须保留创建、更新、删除审计字段。
+
+### 12.2 对 API 接口设计文档
+
+- 必须按“主数据接口、团队接口、状态动作接口、需求接口”拆分。
+- 状态动作单独建接口,不与通用编辑混用。
+- 删除、归档、暂停、恢复、废弃、关闭动作必须定义审计参数或服务端补全规则。
+
+### 12.3 对项目管理业务设计
+
+- 项目必须归属产品。
+- 项目创建时的产品选择器范围按本文件规则执行。
+- 项目成员必须来源于所属产品的有效产品团队成员池。
+- 项目创建后不允许改挂产品。
+
+### 12.4 对权限设计
+
+- 需落实 `product_view`、`product_create`、`product_edit`、`product_status_manage`、`product_delete`、`product_member_manage`、`product_requirement_manage`、`project_create_under_product` 等动作权限。
+- 需落实产品可见范围、建项目资格范围、项目成员来源范围三套口径分离但联动的实现方式。
diff --git a/rdms-project/rdms-project-boot/product/03-工单到任务全链路与工作流方案.md b/rdms-project/rdms-project-boot/product/03-工单到任务全链路与工作流方案.md
new file mode 100644
index 0000000..75469dc
--- /dev/null
+++ b/rdms-project/rdms-project-boot/product/03-工单到任务全链路与工作流方案.md
@@ -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 的开发顺序。
diff --git a/rdms-project/rdms-project-boot/product/04-产品管理_编码前必看清单.md b/rdms-project/rdms-project-boot/product/04-产品管理_编码前必看清单.md
new file mode 100644
index 0000000..49ffbf2
--- /dev/null
+++ b/rdms-project/rdms-project-boot/product/04-产品管理_编码前必看清单.md
@@ -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 落地代码。
+- 不要在产品模块里扩展未确认的版本、路线图、附件等能力。
diff --git a/rdms-project/rdms-project-boot/product/05-产品管理_前端联调最小闭环清单.md b/rdms-project/rdms-project-boot/product/05-产品管理_前端联调最小闭环清单.md
new file mode 100644
index 0000000..6887acb
--- /dev/null
+++ b/rdms-project/rdms-project-boot/product/05-产品管理_前端联调最小闭环清单.md
@@ -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`
diff --git a/rdms-project/rdms-project-boot/product/06-产品主数据_API接口文档.md b/rdms-project/rdms-project-boot/product/06-产品主数据_API接口文档.md
new file mode 100644
index 0000000..147635a
--- /dev/null
+++ b/rdms-project/rdms-project-boot/product/06-产品主数据_API接口文档.md
@@ -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`:
+
+```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`。
+- 若产品方向字典值未准备好,创建和更新接口会触发字典校验失败。
diff --git a/rdms-project/rdms-project-boot/product/rdms_biz_audit_log.sql b/rdms-project/rdms-project-boot/product/rdms_biz_audit_log.sql
new file mode 100644
index 0000000..8da9a5d
--- /dev/null
+++ b/rdms-project/rdms-project-boot/product/rdms_biz_audit_log.sql
@@ -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_code,object_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_code,object_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;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java
new file mode 100644
index 0000000..f8c939e
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java
@@ -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);
+ }
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/api/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/api/package-info.java
new file mode 100644
index 0000000..ab0fbff
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/api/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Project API 实现包,放置对外暴露 RPC 接口的实现类
+ */
+package com.njcn.rdms.module.project.api;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/package-info.java
new file mode 100644
index 0000000..cb4255e
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 管理端控制器包
+ */
+package com.njcn.rdms.module.project.controller.admin;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java
new file mode 100644
index 0000000..83e69a5
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java
@@ -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 createProduct(@Valid @RequestBody ProductSaveReqVO createReqVO) {
+ return success(productService.createProduct(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新产品")
+ @PreAuthorize("@ss.hasPermission('project:product:update')")
+ public CommonResult 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 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> getProductPage(@Valid ProductPageReqVO pageReqVO) {
+ PageResult pageResult = productService.getProductPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, ProductRespVO.class));
+ }
+
+ @PostMapping("/change-status")
+ @Operation(summary = "变更产品状态")
+ @PreAuthorize("@ss.hasPermission('project:product:status')")
+ public CommonResult changeProductStatus(@Valid @RequestBody ProductStatusActionReqVO reqVO) {
+ productService.changeProductStatus(reqVO);
+ return success(true);
+ }
+
+ @PostMapping("/delete")
+ @Operation(summary = "删除产品")
+ @PreAuthorize("@ss.hasPermission('project:product:delete')")
+ public CommonResult deleteProduct(@Valid @RequestBody ProductDeleteReqVO reqVO) {
+ productService.deleteProduct(reqVO);
+ return success(true);
+ }
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductDeleteReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductDeleteReqVO.java
new file mode 100644
index 0000000..e93dba4
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductDeleteReqVO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductPageReqVO.java
new file mode 100644
index 0000000..8da6b28
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductPageReqVO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductRespVO.java
new file mode 100644
index 0000000..e56a55a
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductRespVO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductSaveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductSaveReqVO.java
new file mode 100644
index 0000000..1066cbe
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductSaveReqVO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductStatusActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductStatusActionReqVO.java
new file mode 100644
index 0000000..7b748a6
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductStatusActionReqVO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/app/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/app/package-info.java
new file mode 100644
index 0000000..cb7fb06
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/app/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 应用端控制器包
+ */
+package com.njcn.rdms.module.project.controller.app;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/package-info.java
new file mode 100644
index 0000000..93e6daf
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/package-info.java
@@ -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;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/convert/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/convert/package-info.java
new file mode 100644
index 0000000..fe7117c
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/convert/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * DTO、VO、DO 等对象转换包
+ */
+package com.njcn.rdms.module.project.convert;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/audit/BizAuditLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/audit/BizAuditLogDO.java
new file mode 100644
index 0000000..e105a8a
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/audit/BizAuditLogDO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/package-info.java
new file mode 100644
index 0000000..3891ec4
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 数据对象包
+ */
+package com.njcn.rdms.module.project.dal.dataobject;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductDO.java
new file mode 100644
index 0000000..fe287f6
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductDO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductStatusLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductStatusLogDO.java
new file mode 100644
index 0000000..d8d4b89
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/product/ProductStatusLogDO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/status/ObjectStatusModelDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/status/ObjectStatusModelDO.java
new file mode 100644
index 0000000..bab6f47
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/status/ObjectStatusModelDO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/status/ObjectStatusTransitionDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/status/ObjectStatusTransitionDO.java
new file mode 100644
index 0000000..7e92f4a
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/status/ObjectStatusTransitionDO.java
@@ -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;
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/audit/BizAuditLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/audit/BizAuditLogMapper.java
new file mode 100644
index 0000000..71c79e3
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/audit/BizAuditLogMapper.java
@@ -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 {
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/package-info.java
new file mode 100644
index 0000000..9b3d601
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * MyBatis Mapper 包
+ */
+package com.njcn.rdms.module.project.dal.mysql;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductMapper.java
new file mode 100644
index 0000000..7d26e0b
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductMapper.java
@@ -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 {
+
+ default PageResult selectPage(ProductPageReqVO reqVO) {
+ LambdaQueryWrapperX 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 selectListByCodePrefix(String codePrefix) {
+ return selectList(new LambdaQueryWrapperX()
+ .likeRight(ProductDO::getCode, codePrefix)
+ .orderByDesc(ProductDO::getCode));
+ }
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductStatusLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductStatusLogMapper.java
new file mode 100644
index 0000000..699ff00
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/product/ProductStatusLogMapper.java
@@ -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 {
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/status/ObjectStatusModelMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/status/ObjectStatusModelMapper.java
new file mode 100644
index 0000000..d9d07bd
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/status/ObjectStatusModelMapper.java
@@ -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 {
+
+ default ObjectStatusModelDO selectByObjectTypeAndStatusCode(String objectType, String statusCode) {
+ return selectOne(new LambdaQueryWrapperX()
+ .eq(ObjectStatusModelDO::getObjectType, objectType)
+ .eq(ObjectStatusModelDO::getStatusCode, statusCode));
+ }
+
+ default List selectListByObjectType(String objectType) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(ObjectStatusModelDO::getObjectType, objectType)
+ .orderByAsc(ObjectStatusModelDO::getSort));
+ }
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/status/ObjectStatusTransitionMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/status/ObjectStatusTransitionMapper.java
new file mode 100644
index 0000000..067dd60
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/status/ObjectStatusTransitionMapper.java
@@ -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 {
+
+ default ObjectStatusTransitionDO selectByObjectTypeAndFromStatusAndAction(String objectType,
+ String fromStatusCode,
+ String actionCode) {
+ return selectOne(new LambdaQueryWrapperX()
+ .eq(ObjectStatusTransitionDO::getObjectType, objectType)
+ .eq(ObjectStatusTransitionDO::getFromStatusCode, fromStatusCode)
+ .eq(ObjectStatusTransitionDO::getActionCode, actionCode)
+ .eq(ObjectStatusTransitionDO::getStatus, 0));
+ }
+
+ default List selectListByObjectTypeAndFromStatus(String objectType, String fromStatusCode) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(ObjectStatusTransitionDO::getObjectType, objectType)
+ .eq(ObjectStatusTransitionDO::getFromStatusCode, fromStatusCode)
+ .eq(ObjectStatusTransitionDO::getStatus, 0));
+ }
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/package-info.java
new file mode 100644
index 0000000..b6da628
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 持久层包
+ */
+package com.njcn.rdms.module.project.dal;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java
new file mode 100644
index 0000000..00b1cc7
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java
@@ -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 {
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/config/SecurityConfiguration.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/config/SecurityConfiguration.java
new file mode 100644
index 0000000..839ed61
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/security/config/SecurityConfiguration.java
@@ -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.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();
+ }
+
+ };
+ }
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/package-info.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/package-info.java
new file mode 100644
index 0000000..7a50714
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 服务层包
+ */
+package com.njcn.rdms.module.project.service;
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductService.java
new file mode 100644
index 0000000..9e2be4f
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductService.java
@@ -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 getProductPage(ProductPageReqVO pageReqVO);
+
+ /**
+ * 变更产品状态
+ *
+ * @param reqVO 状态动作请求
+ */
+ void changeProductStatus(ProductStatusActionReqVO reqVO);
+
+ /**
+ * 删除产品
+ *
+ * @param reqVO 删除请求
+ */
+ void deleteProduct(ProductDeleteReqVO reqVO);
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductServiceImpl.java
new file mode 100644
index 0000000..68fd2c1
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductServiceImpl.java
@@ -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 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 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 valueOf(ProductDO product, Function getter) {
+ return product == null ? null : getter.apply(product);
+ }
+
+ private void appendFieldChange(Map fieldChanges, String fieldName, Object before, Object after) {
+ if (Objects.equals(before, after)) {
+ return;
+ }
+ Map 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 : "";
+ }
+
+}
diff --git a/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml b/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml
new file mode 100644
index 0000000..0f82dcd
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml
@@ -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 # 开启演示模式
diff --git a/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml b/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml
new file mode 100644
index 0000000..2fc5cb2
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml
@@ -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
diff --git a/rdms-project/rdms-project-boot/src/main/resources/application.yaml b/rdms-project/rdms-project-boot/src/main/resources/application.yaml
new file mode 100644
index 0000000..1af3ec4
--- /dev/null
+++ b/rdms-project/rdms-project-boot/src/main/resources/application.yaml
@@ -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
diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/ErrorCodeConstants.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/ErrorCodeConstants.java
index 19df4c9..7841cb8 100644
--- a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/ErrorCodeConstants.java
+++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/ErrorCodeConstants.java
@@ -31,6 +31,7 @@ public interface ErrorCodeConstants {
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_NAME_DUPLICATE = new ErrorCode(1_002_001_011, "路由名重复,请检查菜单数据:{}");
+ ErrorCode MENU_SCOPE_NOT_MATCH = new ErrorCode(1_002_001_012, "菜单【{}】不属于当前作用域");
// ========== 角色模块 1-002-002-000 ==========
ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在");
@@ -39,6 +40,7 @@ public interface ErrorCodeConstants {
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_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 ==========
diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/permission/PermissionScopeTypeEnum.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/permission/PermissionScopeTypeEnum.java
new file mode 100644
index 0000000..27fad27
--- /dev/null
+++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/permission/PermissionScopeTypeEnum.java
@@ -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;
+
+}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/AuthController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/AuthController.java
index 1676d58..c6719b1 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/AuthController.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/auth/AuthController.java
@@ -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.user.AdminUserDO;
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.permission.MenuService;
import com.njcn.rdms.module.system.service.permission.PermissionService;
@@ -54,6 +55,9 @@ import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.
@Slf4j
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
private AdminAuthService authService;
@Resource
@@ -154,7 +158,7 @@ public class AuthController {
return Collections.emptyList();
}
- List roles = roleService.getRoleList(roleIds);
+ List roles = roleService.getRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus()));
return roles;
}
@@ -164,8 +168,9 @@ public class AuthController {
return Collections.emptyList();
}
- Set menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
- List menuList = menuService.getMenuList(menuIds);
+ Set menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId),
+ GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ List menuList = menuService.getMenuList(menuIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
return menuService.filterDisableMenus(menuList);
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/MenuController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/MenuController.java
index 5384dcf..dac490e 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/MenuController.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/MenuController.java
@@ -1,13 +1,16 @@
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.pojo.CommonResult;
+import com.njcn.rdms.framework.common.util.validation.ValidationUtils;
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.MenuRespVO;
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.dal.dataobject.permission.MenuDO;
+import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.service.permission.MenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -36,6 +39,10 @@ import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Validated
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
private MenuService menuService;
@@ -43,7 +50,7 @@ public class MenuController {
@Operation(summary = "创建菜单")
@PreAuthorize("@ss.hasPermission('system:menu:create')")
public CommonResult createMenu(@Valid @RequestBody MenuSaveVO createReqVO) {
- Long menuId = menuService.createMenu(createReqVO);
+ Long menuId = menuService.createMenu(normalizeScopeReqVO(createReqVO));
return success(menuId);
}
@@ -77,15 +84,20 @@ public class MenuController {
@Operation(summary = "获取菜单列表", description = "用于【菜单管理】界面")
@PreAuthorize("@ss.hasPermission('system:menu:query')")
public CommonResult> getMenuList(MenuListReqVO reqVO) {
- List list = menuService.getMenuList(reqVO);
+ MenuListReqVO effectiveReqVO = normalizeScopeReqVO(reqVO);
+ List list = menuService.getMenuList(effectiveReqVO,
+ effectiveReqVO.getScopeType(), effectiveReqVO.getObjectType());
list.sort(Comparator.comparing(MenuDO::getSort));
return success(BeanUtils.toBean(list, MenuRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获取菜单精简信息列表", description = "只包含已启用的菜单,用于【角色分配菜单】功能的选项")
- public CommonResult> getSimpleMenuList() {
- List list = menuService.getMenuList(new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()));
+ public CommonResult> getSimpleMenuList(MenuListReqVO reqVO) {
+ MenuListReqVO effectiveReqVO = normalizeScopeReqVO(reqVO);
+ effectiveReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
+ List list = menuService.getMenuList(effectiveReqVO,
+ effectiveReqVO.getScopeType(), effectiveReqVO.getObjectType());
list = menuService.filterDisableMenus(list);
list.sort(Comparator.comparing(MenuDO::getSort));
return success(BeanUtils.toBean(list, MenuSimpleRespVO.class));
@@ -99,4 +111,30 @@ public class MenuController {
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;
+ }
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/RoleController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/RoleController.java
index 3423788..8795003 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/RoleController.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/RoleController.java
@@ -1,16 +1,19 @@
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.common.enums.CommonStatusEnum;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.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.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.RoleSaveReqVO;
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 io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -36,6 +39,10 @@ import static java.util.Collections.singleton;
@Validated
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
private RoleService roleService;
@@ -43,7 +50,7 @@ public class RoleController {
@Operation(summary = "创建角色")
@PreAuthorize("@ss.hasPermission('system:role:create')")
public CommonResult createRole(@Valid @RequestBody RoleSaveReqVO createReqVO) {
- return success(roleService.createRole(createReqVO, null));
+ return success(roleService.createRole(normalizeScopeReqVO(createReqVO), null));
}
@PutMapping("/update")
@@ -84,15 +91,19 @@ public class RoleController {
@Operation(summary = "获得角色分页")
@PreAuthorize("@ss.hasPermission('system:role:query')")
public CommonResult> getRolePage(RolePageReqVO pageReqVO) {
- pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
- PageResult pageResult = roleService.getRolePage(pageReqVO);
+ RolePageReqVO effectiveReqVO = normalizeScopeReqVO(pageReqVO);
+ effectiveReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+ PageResult pageResult = roleService.getRolePage(effectiveReqVO,
+ effectiveReqVO.getScopeType(), effectiveReqVO.getObjectType());
return success(BeanUtils.toBean(pageResult, RoleRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获取角色精简信息列表", description = "只包含被开启的角色,主要用于前端的下拉选项")
- public CommonResult> getSimpleRoleList() {
- List list = roleService.getRoleListByStatus(singleton(CommonStatusEnum.ENABLE.getStatus()));
+ public CommonResult> getSimpleRoleList(RolePageReqVO reqVO) {
+ RolePageReqVO effectiveReqVO = normalizeScopeReqVO(reqVO);
+ List list = roleService.getRoleListByStatus(singleton(CommonStatusEnum.ENABLE.getStatus()),
+ effectiveReqVO.getScopeType(), effectiveReqVO.getObjectType());
list.sort(Comparator.comparing(RoleDO::getSort));
return success(BeanUtils.toBean(list, RoleRespVO.class));
}
@@ -109,4 +120,30 @@ public class RoleController {
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;
+ }
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java
index d744f69..236641c 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java
@@ -1,6 +1,9 @@
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 jakarta.validation.constraints.AssertTrue;
import lombok.Data;
@Schema(description = "管理后台 - 菜单列表 Request VO")
@@ -13,4 +16,23 @@ public class MenuListReqVO {
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
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);
+ }
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java
index 73cd826..f35b1d6 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java
@@ -1,6 +1,9 @@
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 jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -68,4 +71,23 @@ public class MenuSaveVO {
@Schema(description = "是否总是显示", example = "false")
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);
+ }
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RolePageReqVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RolePageReqVO.java
index 5467207..008da14 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RolePageReqVO.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RolePageReqVO.java
@@ -1,7 +1,10 @@
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.module.system.enums.permission.PermissionScopeTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
@@ -28,4 +31,23 @@ public class RolePageReqVO extends PageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
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);
+ }
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java
index 6a481b1..48d1cec 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java
@@ -1,9 +1,12 @@
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.validation.InEnum;
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 jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -44,4 +47,23 @@ public class RoleSaveReqVO {
@DiffLogField(name = "备注")
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);
+ }
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/permission/MenuDO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/permission/MenuDO.java
index e3032cb..f87f01d 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/permission/MenuDO.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/permission/MenuDO.java
@@ -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.mybatis.core.dataobject.BaseDO;
+import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -45,6 +46,18 @@ public class MenuDO extends BaseDO {
* - 对于前端,配合前端标签,配置按钮是否展示,避免用户没有该权限时,结果可以看到该操作。
*/
private String permission;
+ /**
+ * 作用域类型
+ *
+ * 枚举 {@link PermissionScopeTypeEnum}
+ */
+ private String scopeType;
+ /**
+ * 对象类型
+ *
+ * 全局资源固定为空字符串
+ */
+ private String objectType;
/**
* 菜单类型
*
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/permission/RoleDO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/permission/RoleDO.java
index c7af38d..0f83d3a 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/permission/RoleDO.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/permission/RoleDO.java
@@ -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.mybatis.core.dataobject.BaseDO;
+import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import com.njcn.rdms.module.system.enums.permission.RoleTypeEnum;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -33,6 +34,18 @@ public class RoleDO extends BaseDO {
* 枚举
*/
private String code;
+ /**
+ * 作用域类型
+ *
+ * 枚举 {@link PermissionScopeTypeEnum}
+ */
+ private String scopeType;
+ /**
+ * 对象类型
+ *
+ * 全局角色固定为空字符串
+ */
+ private String objectType;
/**
* 角色排序
*/
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/permission/MenuMapper.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/permission/MenuMapper.java
index 6bbc55e..2986a47 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/permission/MenuMapper.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/permission/MenuMapper.java
@@ -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.dal.dataobject.permission.MenuDO;
import org.apache.ibatis.annotations.Mapper;
+import org.springframework.lang.Nullable;
import java.util.List;
@@ -12,7 +13,15 @@ import java.util.List;
public interface MenuMapper extends BaseMapperX {
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()
+ .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) {
@@ -20,17 +29,37 @@ public interface MenuMapper extends BaseMapperX {
}
default List selectList(MenuListReqVO reqVO) {
+ return selectList(reqVO, null, null);
+ }
+
+ default List selectList(MenuListReqVO reqVO, @Nullable String scopeType, @Nullable String objectType) {
return selectList(new LambdaQueryWrapperX()
.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 selectListByPermission(String permission) {
- return selectList(MenuDO::getPermission, permission);
+ return selectListByPermission(permission, null, null);
+ }
+
+ default List selectListByPermission(String permission, @Nullable String scopeType, @Nullable String objectType) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(MenuDO::getPermission, permission)
+ .eq(scopeType != null, MenuDO::getScopeType, scopeType)
+ .eq(objectType != null, MenuDO::getObjectType, objectType));
}
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()
+ .eq(MenuDO::getComponentName, componentName)
+ .eq(scopeType != null, MenuDO::getScopeType, scopeType)
+ .eq(objectType != null, MenuDO::getObjectType, objectType));
}
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/permission/RoleMapper.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/permission/RoleMapper.java
index 00bd154..d19a26b 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/permission/RoleMapper.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/permission/RoleMapper.java
@@ -17,6 +17,10 @@ import java.util.List;
public interface RoleMapper extends BaseMapperX {
default PageResult selectPage(RolePageReqVO reqVO) {
+ return selectPageByScope(reqVO, null, null);
+ }
+
+ default PageResult selectPageByScope(RolePageReqVO reqVO, @Nullable String scopeType, @Nullable String objectType) {
LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>();
boolean hasName = StringUtils.hasText(reqVO.getName());
boolean hasCode = StringUtils.hasText(reqVO.getCode());
@@ -29,21 +33,47 @@ public interface RoleMapper extends BaseMapperX {
.likeIfPresent(RoleDO::getCode, reqVO.getCode());
}
queryWrapper.eqIfPresent(RoleDO::getStatus, reqVO.getStatus())
+ .eq(scopeType != null, RoleDO::getScopeType, scopeType)
+ .eq(objectType != null, RoleDO::getObjectType, objectType)
.betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime())
.orderByAsc(RoleDO::getSort);
- return selectPage(reqVO, queryWrapper);
+ return BaseMapperX.super.selectPage(reqVO, queryWrapper);
}
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()
+ .eq(RoleDO::getName, name)
+ .eq(scopeType != null, RoleDO::getScopeType, scopeType)
+ .eq(objectType != null, RoleDO::getObjectType, objectType));
}
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()
+ .eq(RoleDO::getCode, code)
+ .eq(scopeType != null, RoleDO::getScopeType, scopeType)
+ .eq(objectType != null, RoleDO::getObjectType, objectType));
}
default List selectListByStatus(@Nullable Collection statuses) {
- return selectList(RoleDO::getStatus, statuses);
+ return selectListByStatus(statuses, null, null);
+ }
+
+ default List selectListByStatus(@Nullable Collection statuses,
+ @Nullable String scopeType,
+ @Nullable String objectType) {
+ return selectList(new LambdaQueryWrapperX()
+ .inIfPresent(RoleDO::getStatus, statuses)
+ .eq(scopeType != null, RoleDO::getScopeType, scopeType)
+ .eq(objectType != null, RoleDO::getObjectType, objectType)
+ .orderByAsc(RoleDO::getSort));
}
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/MenuService.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/MenuService.java
index e4c6223..fa9977e 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/MenuService.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/MenuService.java
@@ -23,12 +23,42 @@ public interface MenuService {
List getMenuList(MenuListReqVO reqVO);
+ /**
+ * 获得指定作用域下的菜单列表
+ *
+ * @param reqVO 菜单查询条件
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ * @return 菜单列表
+ */
+ List getMenuList(MenuListReqVO reqVO, String scopeType, String objectType);
+
List getMenuIdListByPermissionFromCache(String permission);
+ /**
+ * 从缓存中获得指定作用域下的权限菜单编号集合
+ *
+ * @param permission 权限标识
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ * @return 菜单编号集合
+ */
+ List getMenuIdListByPermissionFromCache(String permission, String scopeType, String objectType);
+
MenuDO getMenu(Long id);
List getMenuList(Collection ids);
+ /**
+ * 获得指定作用域下的菜单列表
+ *
+ * @param ids 菜单编号数组
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ * @return 菜单列表
+ */
+ List getMenuList(Collection ids, String scopeType, String objectType);
+
/**
* 校验菜单们是否有效。如下情况,视为无效:
* 1. 菜单编号不存在
@@ -38,4 +68,13 @@ public interface MenuService {
*/
void validateMenuList(Collection ids);
+ /**
+ * 校验指定作用域下的菜单们是否有效
+ *
+ * @param ids 菜单编号数组
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ */
+ void validateMenuList(Collection ids, String scopeType, String objectType);
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/MenuServiceImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/MenuServiceImpl.java
index fd123c1..53591ca 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/MenuServiceImpl.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/MenuServiceImpl.java
@@ -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.enums.permission.MenuRouteKindEnum;
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.collect.Lists;
@@ -25,6 +26,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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.util.collection.CollectionUtils.convertList;
@@ -41,6 +43,10 @@ import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.*;
@Slf4j
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
private MenuMapper menuMapper;
@Resource
@@ -48,18 +54,23 @@ public class MenuServiceImpl implements MenuService {
@Override
- @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#createReqVO.permission",
- condition = "#createReqVO.permission != null")
+ @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, allEntries = true)
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);
- validateMenuComponentName(createReqVO.getComponentName(), null);
+ validateMenuName(createReqVO.getParentId(), createReqVO.getName(), null,
+ scopeType, objectType);
+ validateMenuComponentName(createReqVO.getComponentName(), null,
+ scopeType, objectType);
validateMenuRoute(createReqVO);
// 插入数据库
MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class);
+ menu.setScopeType(scopeType);
+ menu.setObjectType(objectType);
initMenuProperty(menu);
menuMapper.insert(menu);
// 返回
@@ -71,18 +82,24 @@ public class MenuServiceImpl implements MenuService {
allEntries = true) // allEntries 清空所有缓存,因为 permission 如果变更,涉及到新老两个 permission。直接清理,简单有效
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);
}
// 校验父菜单存在
- validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId());
+ validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId(),
+ menu.getScopeType(), menu.getObjectType());
// 校验菜单(自己)
- validateMenuName(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId());
- validateMenuComponentName(updateReqVO.getComponentName(), updateReqVO.getId());
+ validateMenuName(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId(),
+ menu.getScopeType(), menu.getObjectType());
+ validateMenuComponentName(updateReqVO.getComponentName(), updateReqVO.getId(),
+ menu.getScopeType(), menu.getObjectType());
validateMenuRoute(updateReqVO);
// 更新到数据库
MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class);
+ updateObj.setScopeType(menu.getScopeType());
+ updateObj.setObjectType(menu.getObjectType());
initMenuProperty(updateObj);
menuMapper.updateById(updateObj);
}
@@ -129,7 +146,7 @@ public class MenuServiceImpl implements MenuService {
@Override
public List getMenuList() {
- return menuMapper.selectList();
+ return getMenuList(new MenuListReqVO(), GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
@@ -180,13 +197,25 @@ public class MenuServiceImpl implements MenuService {
@Override
public List getMenuList(MenuListReqVO reqVO) {
- return menuMapper.selectList(reqVO);
+ return getMenuList(reqVO, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ }
+
+ @Override
+ public List getMenuList(MenuListReqVO reqVO, String scopeType, String objectType) {
+ return menuMapper.selectList(reqVO, scopeType, objectType);
}
@Override
@Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission")
public List getMenuIdListByPermissionFromCache(String permission) {
- List 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 getMenuIdListByPermissionFromCache(String permission, String scopeType, String objectType) {
+ List menus = menuMapper.selectListByPermission(permission, scopeType, objectType);
return convertList(menus, MenuDO::getId);
}
@@ -201,11 +230,28 @@ public class MenuServiceImpl implements MenuService {
if (CollUtil.isEmpty(ids)) {
return Lists.newArrayList();
}
- return menuMapper.selectByIds(ids);
+ List menus = menuMapper.selectByIds(ids);
+ menus.removeIf(menu -> !matchesScope(menu, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE));
+ return menus;
+ }
+
+ @Override
+ public List getMenuList(Collection ids, String scopeType, String objectType) {
+ if (CollUtil.isEmpty(ids)) {
+ return Lists.newArrayList();
+ }
+ List menus = menuMapper.selectByIds(ids);
+ menus.removeIf(menu -> !matchesScope(menu, scopeType, objectType));
+ return menus;
}
@Override
public void validateMenuList(Collection ids) {
+ validateMenuList(ids, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ }
+
+ @Override
+ public void validateMenuList(Collection ids, String scopeType, String objectType) {
if (CollUtil.isEmpty(ids)) {
return;
}
@@ -216,6 +262,9 @@ public class MenuServiceImpl implements MenuService {
if (menu == null) {
throw exception(MENU_NOT_EXISTS);
}
+ if (!matchesScope(menu, scopeType, objectType)) {
+ throw exception(MENU_SCOPE_NOT_MATCH, menu.getName());
+ }
if (CommonStatusEnum.isDisable(menu.getStatus())) {
throw exception(MENU_NOT_ENABLE, menu.getName());
}
@@ -234,6 +283,11 @@ public class MenuServiceImpl implements MenuService {
*/
@VisibleForTesting
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)) {
return;
}
@@ -246,6 +300,9 @@ public class MenuServiceImpl implements MenuService {
if (menu == null) {
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())
&& !MenuTypeEnum.MENU.getType().equals(menu.getType())) {
@@ -264,7 +321,12 @@ public class MenuServiceImpl implements MenuService {
*/
@VisibleForTesting
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) {
return;
}
@@ -285,10 +347,15 @@ public class MenuServiceImpl implements MenuService {
*/
@VisibleForTesting
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)) {
return;
}
- MenuDO menu = menuMapper.selectByComponentName(componentName);
+ MenuDO menu = menuMapper.selectByComponentName(componentName, scopeType, objectType);
if (menu == null) {
return;
}
@@ -364,4 +431,10 @@ public class MenuServiceImpl implements MenuService {
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()));
+ }
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/PermissionService.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/PermissionService.java
index 8f419b0..62efb06 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/PermissionService.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/PermissionService.java
@@ -73,6 +73,18 @@ public interface PermissionService {
*/
Set getRoleMenuListByRoleId(Collection roleIds);
+ /**
+ * 获得指定作用域下的角色们拥有的菜单编号集合
+ *
+ * @param roleIds 角色编号数组
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ * @return 菜单编号集合
+ */
+ default Set getRoleMenuListByRoleId(Collection roleIds, String scopeType, String objectType) {
+ return getRoleMenuListByRoleId(roleIds);
+ }
+
/**
* 获得拥有指定菜单的角色编号数组,从缓存中获取
*
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/PermissionServiceImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/PermissionServiceImpl.java
index 96b70ba..e0f9f4c 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/PermissionServiceImpl.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/PermissionServiceImpl.java
@@ -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.UserRoleMapper;
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.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.google.common.annotations.VisibleForTesting;
@@ -38,6 +39,9 @@ import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.con
@Slf4j
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
private RoleMenuMapper roleMenuMapper;
@Resource
@@ -82,12 +86,13 @@ public class PermissionServiceImpl implements PermissionService {
* @return 是否拥有
*/
private boolean hasAnyPermission(List roles, String permission) {
- List menuIds = menuService.getMenuIdListByPermissionFromCache(permission);
+ List menuIds = menuService.getMenuIdListByPermissionFromCache(permission,
+ GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
// 采用严格模式,如果权限找不到对应的 Menu 的话,也认为没有权限
if (CollUtil.isEmpty(menuIds)) {
return false;
}
- List menus = getEnablePermissionMenus(menuIds);
+ List menus = getEnablePermissionMenus(menuIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
if (CollUtil.isEmpty(menus)) {
return false;
}
@@ -108,12 +113,12 @@ public class PermissionServiceImpl implements PermissionService {
/**
* 加载权限菜单自身及其父链后,再统一过滤禁用节点,避免仅查询按钮节点时误判父菜单缺失。
*/
- private List getEnablePermissionMenus(Collection menuIds) {
+ private List getEnablePermissionMenus(Collection menuIds, String scopeType, String objectType) {
Set targetMenuIds = new HashSet<>(menuIds);
Map menuMap = new LinkedHashMap<>();
Set currentIds = new HashSet<>(menuIds);
while (CollUtil.isNotEmpty(currentIds)) {
- List currentMenus = menuService.getMenuList(currentIds);
+ List currentMenus = menuService.getMenuList(currentIds, scopeType, objectType);
if (CollUtil.isEmpty(currentMenus)) {
break;
}
@@ -131,15 +136,15 @@ public class PermissionServiceImpl implements PermissionService {
/**
* 为已选菜单补齐父链,避免只授权子菜单或按钮时,权限树缺少上级节点。
*/
- private Set expandMenuIdsWithAncestors(Collection menuIds) {
+ private Set expandMenuIdsWithAncestors(Collection menuIds, String scopeType, String objectType) {
Set results = new LinkedHashSet<>(menuIds);
menuIds.forEach(menuId -> {
- MenuDO menu = menuService.getMenu(menuId);
+ MenuDO menu = getMenu(menuId, scopeType, objectType);
while (menu != null && !MenuDO.ID_ROOT.equals(menu.getParentId())) {
if (!results.add(menu.getParentId())) {
break;
}
- menu = menuService.getMenu(menu.getParentId());
+ menu = getMenu(menu.getParentId(), scopeType, objectType);
}
});
return results;
@@ -174,12 +179,14 @@ public class PermissionServiceImpl implements PermissionService {
allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快
})
public void assignRoleMenu(Long roleId, Set menuIds) {
- roleService.validateRoleList(Collections.singleton(roleId));
- menuService.validateMenuList(menuIds);
+ roleService.validateRoleList(Collections.singleton(roleId), GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ RoleDO role = roleService.getRole(roleId);
+ menuService.validateMenuList(menuIds, role.getScopeType(), role.getObjectType());
// 获得角色拥有菜单编号
Set dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId);
// 计算新增和删除的菜单编号
- Set menuIdList = expandMenuIdsWithAncestors(CollUtil.emptyIfNull(menuIds));
+ Set menuIdList = expandMenuIdsWithAncestors(CollUtil.emptyIfNull(menuIds),
+ role.getScopeType(), role.getObjectType());
Collection createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds);
Collection deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList);
// 执行新增和删除。对于已经授权的菜单,不用做任何处理
@@ -222,11 +229,24 @@ public class PermissionServiceImpl implements PermissionService {
if (CollUtil.isEmpty(roleIds)) {
return Collections.emptySet();
}
+ roleService.validateRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ return getRoleMenuListByRoleId(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ }
+
+ @Override
+ public Set getRoleMenuListByRoleId(Collection roleIds, String scopeType, String objectType) {
+ if (CollUtil.isEmpty(roleIds)) {
+ return Collections.emptySet();
+ }
// 统一按角色实际授权返回当前仍然有效的菜单,并补齐其父链
- Set menuIds = convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId);
- List menus = menuService.filterDisableMenus(menuService.getMenuList(menuIds));
- return expandMenuIdsWithAncestors(convertSet(menus, MenuDO::getId));
+ Set scopedRoleIds = convertSet(roleService.getRoleList(roleIds, scopeType, objectType), RoleDO::getId);
+ if (CollUtil.isEmpty(scopedRoleIds)) {
+ return Collections.emptySet();
+ }
+ Set menuIds = convertSet(roleMenuMapper.selectListByRoleId(scopedRoleIds), RoleMenuDO::getMenuId);
+ List menus = menuService.filterDisableMenus(menuService.getMenuList(menuIds, scopeType, objectType));
+ return expandMenuIdsWithAncestors(convertSet(menus, MenuDO::getId), scopeType, objectType);
}
@Override
@@ -242,14 +262,16 @@ public class PermissionServiceImpl implements PermissionService {
@CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public void assignUserRole(Long userId, Set roleIds) {
userService.validateUserList(Collections.singleton(userId));
- roleService.validateRoleList(roleIds);
+ roleService.validateRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
// 获得角色拥有角色编号
Set dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId),
UserRoleDO::getRoleId);
+ Set dbGlobalRoleIds = convertSet(
+ roleService.getRoleList(dbRoleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE), RoleDO::getId);
// 计算新增和删除的角色编号
Set roleIdList = CollUtil.emptyIfNull(roleIds);
- Collection createRoleIds = CollUtil.subtract(roleIdList, dbRoleIds);
- Collection deleteMenuIds = CollUtil.subtract(dbRoleIds, roleIdList);
+ Collection createRoleIds = CollUtil.subtract(roleIdList, dbGlobalRoleIds);
+ Collection deleteRoleIds = CollUtil.subtract(dbGlobalRoleIds, roleIdList);
// 执行新增和删除。对于已经授权的角色,不用做任何处理
if (!CollectionUtil.isEmpty(createRoleIds)) {
userRoleMapper.insertBatch(CollectionUtils.convertList(createRoleIds, roleId -> {
@@ -259,8 +281,8 @@ public class PermissionServiceImpl implements PermissionService {
return entity;
}));
}
- if (!CollectionUtil.isEmpty(deleteMenuIds)) {
- userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteMenuIds);
+ if (!CollectionUtil.isEmpty(deleteRoleIds)) {
+ userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteRoleIds);
}
}
@@ -273,7 +295,7 @@ public class PermissionServiceImpl implements PermissionService {
@Override
public Set getUserRoleIdListByUserId(Long userId) {
Set roleIds = getRawUserRoleIdListByUserId(userId);
- List roles = roleService.getRoleList(roleIds);
+ List roles = roleService.getRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus()));
return convertSet(roles, RoleDO::getId);
}
@@ -281,7 +303,8 @@ public class PermissionServiceImpl implements PermissionService {
@Override
@Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
public Set getUserRoleIdListByUserIdFromCache(Long userId) {
- return getRawUserRoleIdListByUserId(userId);
+ Set roleIds = getRawUserRoleIdListByUserId(userId);
+ return convertSet(roleService.getRoleList(roleIds, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE), RoleDO::getId);
}
@Override
@@ -310,6 +333,11 @@ public class PermissionServiceImpl implements PermissionService {
*
* @return 自己
*/
+ private MenuDO getMenu(Long menuId, String scopeType, String objectType) {
+ List menus = menuService.getMenuList(Collections.singleton(menuId), scopeType, objectType);
+ return CollUtil.isEmpty(menus) ? null : menus.get(0);
+ }
+
private PermissionServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/RoleService.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/RoleService.java
index 57ac9bd..2a0b706 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/RoleService.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/RoleService.java
@@ -70,6 +70,16 @@ public interface RoleService {
*/
List getRoleList(Collection ids);
+ /**
+ * 获得指定作用域下的角色列表
+ *
+ * @param ids 角色编号数组
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ * @return 角色列表
+ */
+ List getRoleList(Collection ids, String scopeType, String objectType);
+
/**
* 获得角色数组,从缓存中
*
@@ -86,6 +96,16 @@ public interface RoleService {
*/
List getRoleListByStatus(Collection statuses);
+ /**
+ * 获得指定作用域下的角色列表
+ *
+ * @param statuses 筛选的状态
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ * @return 角色列表
+ */
+ List getRoleListByStatus(Collection statuses, String scopeType, String objectType);
+
/**
* 获得所有角色列表
*
@@ -101,6 +121,16 @@ public interface RoleService {
*/
PageResult getRolePage(RolePageReqVO reqVO);
+ /**
+ * 获得指定作用域下的角色分页
+ *
+ * @param reqVO 角色分页查询
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ * @return 角色分页结果
+ */
+ PageResult getRolePage(RolePageReqVO reqVO, String scopeType, String objectType);
+
/**
* 判断角色编号数组中,是否有管理员
*
@@ -118,4 +148,13 @@ public interface RoleService {
*/
void validateRoleList(Collection ids);
+ /**
+ * 校验指定作用域下的角色们是否有效
+ *
+ * @param ids 角色编号数组
+ * @param scopeType 作用域类型
+ * @param objectType 对象类型
+ */
+ void validateRoleList(Collection ids, String scopeType, String objectType);
+
}
diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/RoleServiceImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/RoleServiceImpl.java
index 4374a46..1084d06 100644
--- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/RoleServiceImpl.java
+++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/permission/RoleServiceImpl.java
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.annotations.VisibleForTesting;
@@ -21,6 +22,7 @@ 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.UserRoleMapper;
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.RoleTypeEnum;
import jakarta.annotation.Resource;
@@ -35,6 +37,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
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.util.collection.CollectionUtils.convertMap;
@@ -45,6 +48,10 @@ import static com.njcn.rdms.module.system.enums.LogRecordConstants.*;
@Slf4j
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
private PermissionService permissionService;
@@ -59,11 +66,16 @@ public class RoleServiceImpl implements RoleService {
@LogRecord(type = SYSTEM_ROLE_TYPE, subType = SYSTEM_ROLE_CREATE_SUB_TYPE, bizNo = "{{#role.id}}",
success = SYSTEM_ROLE_CREATE_SUCCESS)
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)
.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);
LogRecordContext.putVariable("role", role);
@@ -77,7 +89,8 @@ public class RoleServiceImpl implements RoleService {
public void updateRole(RoleSaveReqVO updateReqVO) {
RoleDO role = validateRoleExists(updateReqVO.getId());
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())) {
@@ -88,6 +101,8 @@ public class RoleServiceImpl implements RoleService {
if (shouldPreserveBuiltInCode(role)) {
updateObj.setCode(role.getCode());
}
+ updateObj.setScopeType(role.getScopeType());
+ updateObj.setObjectType(role.getObjectType());
roleMapper.updateById(updateObj);
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(role, RoleSaveReqVO.class));
@@ -135,6 +150,11 @@ public class RoleServiceImpl implements RoleService {
@VisibleForTesting
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 (id == null) {
throw exception(ROLE_ADMIN_CODE_ERROR, code);
@@ -145,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)) {
throw exception(ROLE_NAME_DUPLICATE, name);
}
@@ -153,7 +173,7 @@ public class RoleServiceImpl implements RoleService {
if (!StringUtils.hasText(code)) {
return;
}
- role = roleMapper.selectByCode(code);
+ role = roleMapper.selectByCode(code, scopeType, objectType);
if (role != null && !role.getId().equals(id)) {
throw exception(ROLE_CODE_DUPLICATE, code);
}
@@ -196,20 +216,32 @@ public class RoleServiceImpl implements RoleService {
@Override
public List getRoleListByStatus(Collection statuses) {
- return roleMapper.selectListByStatus(statuses);
+ return getRoleListByStatus(statuses, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ }
+
+ @Override
+ public List getRoleListByStatus(Collection statuses, String scopeType, String objectType) {
+ return roleMapper.selectListByStatus(statuses, scopeType, objectType);
}
@Override
public List getRoleList() {
- return roleMapper.selectList();
+ return getRoleListByStatus(null, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
}
@Override
public List getRoleList(Collection ids) {
+ return getRoleList(ids, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ }
+
+ @Override
+ public List getRoleList(Collection ids, String scopeType, String objectType) {
if (CollectionUtil.isEmpty(ids)) {
return Collections.emptyList();
}
- return roleMapper.selectByIds(ids);
+ List roles = roleMapper.selectByIds(ids);
+ roles.removeIf(role -> !matchesScope(role, scopeType, objectType));
+ return roles;
}
@Override
@@ -223,7 +255,12 @@ public class RoleServiceImpl implements RoleService {
@Override
public PageResult getRolePage(RolePageReqVO reqVO) {
- return roleMapper.selectPage(reqVO);
+ return getRolePage(reqVO, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ }
+
+ @Override
+ public PageResult getRolePage(RolePageReqVO reqVO, String scopeType, String objectType) {
+ return roleMapper.selectPageByScope(reqVO, scopeType, objectType);
}
@Override
@@ -240,6 +277,11 @@ public class RoleServiceImpl implements RoleService {
@Override
public void validateRoleList(Collection ids) {
+ validateRoleList(ids, GLOBAL_SCOPE_TYPE, GLOBAL_OBJECT_TYPE);
+ }
+
+ @Override
+ public void validateRoleList(Collection ids, String scopeType, String objectType) {
if (CollUtil.isEmpty(ids)) {
return;
}
@@ -250,12 +292,21 @@ public class RoleServiceImpl implements RoleService {
if (role == null) {
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())) {
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() {
return SpringUtil.getBean(getClass());
}
diff --git a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/01_permission_v2_stage1_schema.sql b/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/01_permission_v2_stage1_schema.sql
deleted file mode 100644
index f17961e..0000000
--- a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/01_permission_v2_stage1_schema.sql
+++ /dev/null
@@ -1,127 +0,0 @@
--- permission-v2 stage 1 schema for MySQL 8.0
--- execution target: existing rdms system schema
--- execution mode: manual
---
--- purpose:
--- 1. add target-model columns
--- 2. create visibility relation tables
--- 3. keep legacy fields temporarily for subsequent code migration
---
--- note:
--- 1. run this script before stage 2/3 code changes
--- 2. do not drop leader_user_id / post_ids / data_scope in this stage
-
-START TRANSACTION;
-
-ALTER TABLE system_dept
- ADD COLUMN org_type VARCHAR(20) NOT NULL DEFAULT 'dept' COMMENT '组织节点类型:company/dept/direction/team' AFTER parent_id,
- ADD COLUMN path VARCHAR(1024) CHARACTER SET ascii NOT NULL DEFAULT '/' COMMENT '组织物化路径,格式如 /1/2/3/' AFTER org_type,
- ADD COLUMN level INT NOT NULL DEFAULT 1 COMMENT '组织层级,根节点为 1' AFTER path,
- ADD COLUMN code VARCHAR(64) NULL COMMENT '组织编码' AFTER level;
-
-ALTER TABLE system_users
- ADD COLUMN position_id BIGINT NULL COMMENT '主岗位ID' AFTER dept_id,
- ADD COLUMN resigned_at DATETIME NULL COMMENT '离职时间' AFTER position_id;
-
-ALTER TABLE system_post
- ADD COLUMN post_type VARCHAR(20) NULL COMMENT '岗位类型:management/technical/business' AFTER code,
- ADD COLUMN level_rank INT NULL COMMENT '岗位等级排序,越大级别越高' AFTER post_type;
-
-ALTER TABLE system_dept
- ADD KEY idx_system_dept_parent_id (parent_id),
- ADD KEY idx_system_dept_org_type (org_type),
- ADD KEY idx_system_dept_path (path(191)),
- ADD UNIQUE KEY uk_system_dept_code (code);
-
-ALTER TABLE system_users
- ADD KEY idx_system_users_dept_id (dept_id),
- ADD KEY idx_system_users_position_id (position_id);
-
-ALTER TABLE system_post
- ADD KEY idx_system_post_type (post_type),
- ADD KEY idx_system_post_level_rank (level_rank);
-
-CREATE TABLE system_org_leader_relation (
- id BIGINT NOT NULL COMMENT '主键ID',
- dept_id BIGINT NOT NULL COMMENT '组织节点ID',
- user_id BIGINT NOT NULL COMMENT '负责人用户ID',
- effective_from DATETIME NULL COMMENT '生效开始时间,空表示立即长期生效',
- effective_until DATETIME NULL COMMENT '生效结束时间,空表示长期有效',
- remark VARCHAR(500) NULL COMMENT '备注',
- creator VARCHAR(64) NULL DEFAULT '' COMMENT '创建者',
- create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- updater VARCHAR(64) 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),
- KEY idx_org_leader_user (user_id, deleted, effective_from, effective_until),
- KEY idx_org_leader_dept (dept_id, deleted, effective_from, effective_until),
- UNIQUE KEY uk_org_leader_once (dept_id, user_id, effective_from)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织负责人关系表';
-
-CREATE TABLE system_user_management_relation (
- id BIGINT NOT NULL COMMENT '主键ID',
- manager_user_id BIGINT NOT NULL COMMENT '管理者用户ID',
- subordinate_user_id BIGINT NOT NULL COMMENT '被管理用户ID',
- effective_from DATETIME NULL COMMENT '生效开始时间,空表示立即长期生效',
- effective_until DATETIME NULL COMMENT '生效结束时间,空表示长期有效',
- remark VARCHAR(500) NULL COMMENT '备注',
- creator VARCHAR(64) NULL DEFAULT '' COMMENT '创建者',
- create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- updater VARCHAR(64) 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),
- KEY idx_mgr_user (manager_user_id, deleted, effective_from, effective_until),
- KEY idx_sub_user (subordinate_user_id, deleted, effective_from, effective_until),
- UNIQUE KEY uk_mgr_sub_once (manager_user_id, subordinate_user_id, effective_from)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户直接管理关系表';
-
-CREATE TABLE system_user_visibility_config (
- id BIGINT NOT NULL COMMENT '主键ID',
- user_id BIGINT NOT NULL COMMENT '用户ID',
- visibility_type VARCHAR(20) NOT NULL COMMENT '可见范围类型:all/directions/projects',
- visible_direction_ids JSON NULL COMMENT '补充可见方向ID集合',
- visible_project_ids JSON NULL COMMENT '补充可见项目ID集合',
- remark VARCHAR(500) NULL COMMENT '备注',
- creator VARCHAR(64) NULL DEFAULT '' COMMENT '创建者',
- create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- updater VARCHAR(64) 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),
- UNIQUE KEY uk_visibility_user (user_id),
- KEY idx_visibility_type (visibility_type),
- CONSTRAINT chk_visibility_type CHECK (visibility_type IN ('all', 'directions', 'projects'))
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户补充可见范围配置表';
-
-CREATE TABLE system_project_member (
- id BIGINT NOT NULL COMMENT '主键ID',
- project_id BIGINT NOT NULL COMMENT '项目ID',
- user_id BIGINT NOT NULL COMMENT '用户ID',
- project_role VARCHAR(20) NOT NULL COMMENT '项目角色:pm/product/developer/tester/viewer',
- member_type VARCHAR(20) NOT NULL DEFAULT 'core' COMMENT '成员类型:core/support',
- joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
- left_at DATETIME NULL COMMENT '退出时间,空表示仍在项目中',
- remark VARCHAR(500) NULL COMMENT '备注',
- creator VARCHAR(64) NULL DEFAULT '' COMMENT '创建者',
- create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- updater VARCHAR(64) 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 '是否删除',
- active_user_id BIGINT GENERATED ALWAYS AS (
- CASE WHEN left_at IS NULL THEN user_id ELSE NULL END
- ) STORED,
- active_pm_project_id BIGINT GENERATED ALWAYS AS (
- CASE WHEN project_role = 'pm' AND left_at IS NULL THEN project_id ELSE NULL END
- ) STORED,
- PRIMARY KEY (id),
- UNIQUE KEY uk_project_active_member (project_id, active_user_id),
- UNIQUE KEY uk_project_active_pm (active_pm_project_id),
- KEY idx_pm_user_active (user_id, left_at),
- KEY idx_pm_project_role_active (project_id, project_role, left_at),
- CONSTRAINT chk_project_role CHECK (project_role IN ('pm', 'product', 'developer', 'tester', 'viewer')),
- CONSTRAINT chk_member_type CHECK (member_type IN ('core', 'support'))
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目成员关系表';
-
-COMMIT;
diff --git a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/02_permission_v2_stage1_org_backfill.sql b/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/02_permission_v2_stage1_org_backfill.sql
deleted file mode 100644
index bf204f4..0000000
--- a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/02_permission_v2_stage1_org_backfill.sql
+++ /dev/null
@@ -1,44 +0,0 @@
--- permission-v2 stage 1 organization backfill for MySQL 8.0
--- purpose:
--- 1. initialize org_type
--- 2. backfill path and level from parent_id
---
--- assumption:
--- 1. root organization nodes satisfy parent_id = 0
--- 2. system_dept currently has no parent cycle
-
-START TRANSACTION;
-
-UPDATE system_dept
-SET org_type = 'dept'
-WHERE org_type IS NULL OR org_type = '';
-
-WITH RECURSIVE dept_tree AS (
- SELECT
- id,
- parent_id,
- CAST(CONCAT('/', id, '/') AS CHAR(1024)) AS new_path,
- 1 AS new_level
- FROM system_dept
- WHERE parent_id = 0
-
- UNION ALL
-
- SELECT
- child.id,
- child.parent_id,
- CAST(CONCAT(parent.new_path, child.id, '/') AS CHAR(1024)) AS new_path,
- parent.new_level + 1 AS new_level
- FROM system_dept child
- INNER JOIN dept_tree parent ON child.parent_id = parent.id
-)
-UPDATE system_dept d
-INNER JOIN dept_tree t ON d.id = t.id
-SET
- d.path = t.new_path,
- d.level = t.new_level;
-
-COMMIT;
-
--- verification:
--- SELECT id, name, parent_id, org_type, path, level FROM system_dept ORDER BY parent_id, sort, id;
diff --git a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/03_permission_v2_stage1_seed.sql b/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/03_permission_v2_stage1_seed.sql
deleted file mode 100644
index 39969ad..0000000
--- a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/03_permission_v2_stage1_seed.sql
+++ /dev/null
@@ -1,69 +0,0 @@
--- permission-v2 stage 1 seed for MySQL 8.0
--- purpose:
--- 1. ensure there is at least one root organization node for user association
--- 2. ensure there is at least one available post for user association
--- 3. bind admin user to an organization and a primary position
---
--- note:
--- 1. the reserved ids below are only for cold-start initialization
--- 2. adjust them before execution if your database already uses the same ids
-
-START TRANSACTION;
-
-INSERT INTO system_dept (
- id, name, parent_id, sort, org_type, path, level, code, phone, email, status,
- creator, create_time, updater, update_time, deleted
-)
-SELECT
- 900000000000000001, '平台根组织', 0, 0, 'company', '/900000000000000001/', 1, 'ROOT',
- NULL, NULL, 0,
- 'system', NOW(), 'system', NOW(), b'0'
-FROM DUAL
-WHERE NOT EXISTS (
- SELECT 1 FROM system_dept WHERE parent_id = 0 AND deleted = b'0'
-);
-
-INSERT INTO system_post (
- id, name, code, post_type, level_rank, sort, status, remark,
- creator, create_time, updater, update_time, deleted
-)
-SELECT
- 900000000000000101, '系统管理员岗', 'SYS_ADMIN_POST', 'management', 100, 0, 0, 'permission-v2 cold-start seed',
- 'system', NOW(), 'system', NOW(), b'0'
-FROM DUAL
-WHERE NOT EXISTS (
- SELECT 1 FROM system_post WHERE deleted = b'0'
-);
-
-UPDATE system_users
-SET
- dept_id = COALESCE(
- dept_id,
- (SELECT root_dept.id
- FROM (
- SELECT id
- FROM system_dept
- WHERE parent_id = 0 AND deleted = b'0'
- ORDER BY sort ASC, id ASC
- LIMIT 1
- ) root_dept)
- ),
- position_id = COALESCE(
- position_id,
- (SELECT chosen_post.id
- FROM (
- SELECT id
- FROM system_post
- WHERE deleted = b'0'
- ORDER BY sort ASC, id ASC
- LIMIT 1
- ) chosen_post)
- ),
- update_time = NOW(),
- updater = 'system'
-WHERE username = 'admin';
-
-COMMIT;
-
--- verification:
--- SELECT id, username, dept_id, position_id FROM system_users WHERE username = 'admin';
diff --git a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/04_permission_v2_stage1_finalize.sql b/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/04_permission_v2_stage1_finalize.sql
deleted file mode 100644
index 4c5a4ba..0000000
--- a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/04_permission_v2_stage1_finalize.sql
+++ /dev/null
@@ -1,29 +0,0 @@
--- permission-v2 stage 1 finalize for MySQL 8.0
--- purpose:
--- 1. verify required user-position data is complete
--- 2. convert system_users.position_id to NOT NULL
---
--- execute this script only after:
--- 1. 01/02/03 have completed successfully
--- 2. every existing user has a valid position_id
-
-SELECT COUNT(*) AS missing_position_count
-FROM system_users
-WHERE position_id IS NULL;
-
-SET @missing_position_count := (
- SELECT COUNT(*) FROM system_users WHERE position_id IS NULL
-);
-
-SET @finalize_sql := IF(
- @missing_position_count = 0,
- 'ALTER TABLE system_users MODIFY COLUMN position_id BIGINT NOT NULL COMMENT ''主岗位ID''',
- 'SELECT ''skip finalize: system_users.position_id still has NULL rows'' AS message'
-);
-
-PREPARE stmt FROM @finalize_sql;
-EXECUTE stmt;
-DEALLOCATE PREPARE stmt;
-
--- verification:
--- SHOW CREATE TABLE system_users;
diff --git a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/system_dept.sql b/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/system_dept.sql
deleted file mode 100644
index bcdf45a..0000000
--- a/rdms-system/rdms-system-boot/src/main/resources/sql/permission-v2/system_dept.sql
+++ /dev/null
@@ -1,710 +0,0 @@
-/*
- Navicat Premium Dump SQL
-
- Source Server : localMysql
- Source Server Type : MySQL
- Source Server Version : 80043 (8.0.43)
- Source Host : localhost:13306
- Source Schema : rdms
-
- Target Server Type : MySQL
- Target Server Version : 80043 (8.0.43)
- File Encoding : 65001
-
- Date: 19/03/2026 14:23:55
-*/
-
-SET NAMES utf8mb4;
-SET FOREIGN_KEY_CHECKS = 0;
-
--- ----------------------------
--- Table structure for system_dept
--- ----------------------------
-DROP TABLE IF EXISTS `system_dept`;
-CREATE TABLE `system_dept` (
- `id` bigint NOT NULL COMMENT '部门id',
- `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '部门名称',
- `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父部门id',
- `org_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'dept' COMMENT '组织节点类型:company/dept/direction/team',
- `path` varchar(1024) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '/' COMMENT '组织物化路径,格式如 /1/2/3/',
- `level` int NOT NULL DEFAULT 1 COMMENT '组织层级,根节点为 1',
- `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '组织编码',
- `sort` int NOT NULL DEFAULT 0 COMMENT '显示顺序',
- `leader_user_id` bigint NULL DEFAULT NULL COMMENT '负责人',
- `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系电话',
- `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
- `status` tinyint NOT NULL COMMENT '部门状态(0正常 1停用)',
- `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_system_dept_code`(`code` ASC) USING BTREE,
- INDEX `idx_system_dept_parent_id`(`parent_id` ASC) USING BTREE,
- INDEX `idx_system_dept_org_type`(`org_type` ASC) USING BTREE,
- INDEX `idx_system_dept_path`(`path`(191) ASC) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_dept
--- ----------------------------
-INSERT INTO `system_dept` VALUES (100, '灿能源码', 0, 'dept', '/100/', 1, NULL, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2026-03-19 13:58:49', b'0');
-INSERT INTO `system_dept` VALUES (101, '深圳总公司', 100, 'dept', '/100/101/', 2, NULL, 1, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2026-03-19 14:22:19', b'0');
-INSERT INTO `system_dept` VALUES (103, '研发部门', 101, 'dept', '/100/101/103/', 3, NULL, 1, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2026-03-19 13:58:49', b'0');
-INSERT INTO `system_dept` VALUES (107, '运维部门', 101, 'dept', '/100/101/107/', 3, NULL, 5, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2026-03-19 13:58:49', b'0');
-INSERT INTO `system_dept` VALUES (112, '产品部门', 101, 'dept', '/100/101/112/', 3, NULL, 100, 1, NULL, NULL, 1, '1', '2023-12-02 09:45:13', '1', '2026-03-19 13:58:49', b'0');
-
--- ----------------------------
--- Table structure for system_menu
--- ----------------------------
-DROP TABLE IF EXISTS `system_menu`;
-CREATE TABLE `system_menu` (
- `id` bigint NOT NULL COMMENT '菜单ID',
- `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
- `permission` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限标识',
- `type` tinyint NOT NULL COMMENT '菜单类型',
- `sort` int NOT NULL DEFAULT 0 COMMENT '显示顺序',
- `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父菜单ID',
- `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '路由地址',
- `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '#' COMMENT '菜单图标',
- `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '组件路径',
- `component_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '组件名',
- `status` tinyint NOT NULL DEFAULT 0 COMMENT '菜单状态',
- `visible` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否可见',
- `keep_alive` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否缓存',
- `always_show` bit(1) NOT NULL DEFAULT b'1' 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
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_menu
--- ----------------------------
-INSERT INTO `system_menu` VALUES (1, '系统管理', '', 1, 10, 0, '/system', 'ep:tools', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:27', b'0');
-INSERT INTO `system_menu` VALUES (2, '基础设施', '', 1, 20, 0, '/infra', 'ep:monitor', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-03-01 08:28:40', b'0');
-INSERT INTO `system_menu` VALUES (5, 'OA 示例', '', 1, 40, 1185, 'oa', 'fa:road', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-09-20 16:26:19', '1', '2024-02-29 12:38:13', b'0');
-INSERT INTO `system_menu` VALUES (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:41', b'0');
-INSERT INTO `system_menu` VALUES (101, '角色管理', '', 2, 2, 1, 'role', 'ep:user', 'system/role/index', 'SystemRole', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-05-01 18:35:29', b'0');
-INSERT INTO `system_menu` VALUES (102, '菜单管理', '', 2, 3, 1, 'menu', 'ep:menu', 'system/menu/index', 'SystemMenu', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:03:50', b'0');
-INSERT INTO `system_menu` VALUES (103, '部门管理', '', 2, 4, 1, 'dept', 'fa:address-card', 'system/dept/index', 'SystemDept', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:28', b'0');
-INSERT INTO `system_menu` VALUES (104, '岗位管理', '', 2, 5, 1, 'post', 'fa:address-book-o', 'system/post/index', 'SystemPost', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:39', b'0');
-INSERT INTO `system_menu` VALUES (105, '字典管理', '', 2, 6, 1, 'dict', 'ep:collection', 'system/dict/index', 'SystemDictType', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:07:12', b'0');
-INSERT INTO `system_menu` VALUES (106, '配置管理', '', 2, 8, 2, 'config', 'fa:connectdevelop', 'infra/config/index', 'InfraConfig', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-04-23 00:02:45', b'0');
-INSERT INTO `system_menu` VALUES (108, '审计日志', '', 1, 9, 1, 'log', 'ep:document-copy', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:08:30', b'0');
-INSERT INTO `system_menu` VALUES (109, '令牌管理', '', 2, 2, 1261, 'token', 'fa:key', 'system/oauth2/token/index', 'SystemTokenClient', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:13:48', b'0');
-INSERT INTO `system_menu` VALUES (111, 'MySQL 监控', '', 2, 1, 2740, 'druid', 'fa-solid:box', 'infra/druid/index', 'InfraDruid', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-04-23 00:05:58', b'0');
-INSERT INTO `system_menu` VALUES (112, 'Java 监控', '', 2, 3, 2740, 'admin-server', 'ep:coffee-cup', 'infra/server/index', 'InfraAdminServer', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-04-23 00:06:57', b'0');
-INSERT INTO `system_menu` VALUES (113, 'Redis 监控', '', 2, 2, 2740, 'redis', 'fa:reddit-square', 'infra/redis/index', 'InfraRedis', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-04-23 00:06:09', b'0');
-INSERT INTO `system_menu` VALUES (114, '表单构建', 'infra:build:list', 2, 2, 2, 'build', 'fa:wpforms', 'infra/build/index', 'InfraBuild', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 08:51:35', b'0');
-INSERT INTO `system_menu` VALUES (116, 'API 接口', 'infra:swagger:list', 2, 3, 2, 'swagger', 'fa:fighter-jet', 'infra/swagger/index', 'InfraSwagger', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-04-23 00:01:24', b'0');
-INSERT INTO `system_menu` VALUES (500, '操作日志', '', 2, 1, 108, 'operate-log', 'ep:position', 'system/operatelog/index', 'SystemOperateLog', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:09:59', b'0');
-INSERT INTO `system_menu` VALUES (501, '登录日志', '', 2, 2, 108, 'login-log', 'ep:promotion', 'system/loginlog/index', 'SystemLoginLog', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:10:29', b'0');
-INSERT INTO `system_menu` VALUES (1001, '用户查询', 'system:user:query', 3, 1, 100, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1002, '用户新增', 'system:user:create', 3, 2, 100, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1003, '用户修改', 'system:user:update', 3, 3, 100, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1004, '用户删除', 'system:user:delete', 3, 4, 100, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1005, '用户导出', 'system:user:export', 3, 5, 100, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1006, '用户导入', 'system:user:import', 3, 6, 100, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1007, '重置密码', 'system:user:update-password', 3, 7, 100, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1008, '角色查询', 'system:role:query', 3, 1, 101, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1009, '角色新增', 'system:role:create', 3, 2, 101, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1010, '角色修改', 'system:role:update', 3, 3, 101, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1011, '角色删除', 'system:role:delete', 3, 4, 101, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1012, '角色导出', 'system:role:export', 3, 5, 101, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1013, '菜单查询', 'system:menu:query', 3, 1, 102, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1014, '菜单新增', 'system:menu:create', 3, 2, 102, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1015, '菜单修改', 'system:menu:update', 3, 3, 102, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1016, '菜单删除', 'system:menu:delete', 3, 4, 102, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1017, '部门查询', 'system:dept:query', 3, 1, 103, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1018, '部门新增', 'system:dept:create', 3, 2, 103, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1019, '部门修改', 'system:dept:update', 3, 3, 103, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1020, '部门删除', 'system:dept:delete', 3, 4, 103, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1021, '岗位查询', 'system:post:query', 3, 1, 104, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1022, '岗位新增', 'system:post:create', 3, 2, 104, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1023, '岗位修改', 'system:post:update', 3, 3, 104, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1024, '岗位删除', 'system:post:delete', 3, 4, 104, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1025, '岗位导出', 'system:post:export', 3, 5, 104, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1026, '字典查询', 'system:dict:query', 3, 1, 105, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1027, '字典新增', 'system:dict:create', 3, 2, 105, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1028, '字典修改', 'system:dict:update', 3, 3, 105, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1029, '字典删除', 'system:dict:delete', 3, 4, 105, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1030, '字典导出', 'system:dict:export', 3, 5, 105, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1031, '配置查询', 'infra:config:query', 3, 1, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1032, '配置新增', 'infra:config:create', 3, 2, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1033, '配置修改', 'infra:config:update', 3, 3, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1034, '配置删除', 'infra:config:delete', 3, 4, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1035, '配置导出', 'infra:config:export', 3, 5, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1040, '操作查询', 'system:operate-log:query', 3, 1, 500, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1042, '日志导出', 'system:operate-log:export', 3, 2, 500, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1043, '登录查询', 'system:login-log:query', 3, 1, 501, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1045, '日志导出', 'system:login-log:export', 3, 3, 501, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1046, '令牌列表', 'system:oauth2-token:page', 3, 1, 109, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-05-09 23:54:42', b'0');
-INSERT INTO `system_menu` VALUES (1048, '令牌删除', 'system:oauth2-token:delete', 3, 2, 109, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-05-09 23:54:53', b'0');
-INSERT INTO `system_menu` VALUES (1063, '设置角色菜单权限', 'system:permission:assign-role-menu', 3, 6, 101, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-06 17:53:44', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1064, '设置角色数据权限', 'system:permission:assign-role-data-scope', 3, 7, 101, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-06 17:56:31', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1065, '设置用户角色', 'system:permission:assign-user-role', 3, 8, 101, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-07 10:23:28', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1066, '获得 Redis 监控信息', 'infra:redis:get-monitor-info', 3, 1, 113, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-26 01:02:31', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1067, '获得 Redis Key 列表', 'infra:redis:get-key-list', 3, 2, 113, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-26 01:02:52', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1070, '代码生成案例', '', 1, 1, 2, 'demo', 'ep:aim', 'infra/testDemo/index', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2023-11-15 23:45:53', b'0');
-INSERT INTO `system_menu` VALUES (1077, '链路追踪', '', 2, 4, 2740, 'skywalking', 'fa:eye', 'infra/skywalking/index', 'InfraSkyWalking', 0, b'1', b'1', b'1', '', '2021-02-08 20:41:31', '1', '2024-04-23 00:07:15', b'0');
-INSERT INTO `system_menu` VALUES (1078, '访问日志', '', 2, 1, 1083, 'api-access-log', 'ep:place', 'infra/apiAccessLog/index', 'InfraApiAccessLog', 0, b'1', b'1', b'1', '', '2021-02-26 01:32:59', '1', '2024-02-29 08:54:57', b'0');
-INSERT INTO `system_menu` VALUES (1082, '日志导出', 'infra:api-access-log:export', 3, 2, 1078, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-26 01:32:59', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1083, 'API 日志', '', 2, 4, 2, 'log', 'fa:tasks', NULL, NULL, 0, b'1', b'1', b'1', '', '2021-02-26 02:18:24', '1', '2024-04-22 23:58:36', b'0');
-INSERT INTO `system_menu` VALUES (1084, '错误日志', 'infra:api-error-log:query', 2, 2, 1083, 'api-error-log', 'ep:warning-filled', 'infra/apiErrorLog/index', 'InfraApiErrorLog', 0, b'1', b'1', b'1', '', '2021-02-26 07:53:20', '1', '2024-02-29 08:55:17', b'0');
-INSERT INTO `system_menu` VALUES (1085, '日志处理', 'infra:api-error-log:update-status', 3, 2, 1084, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-26 07:53:20', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1086, '日志导出', 'infra:api-error-log:export', 3, 3, 1084, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-26 07:53:20', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1088, '日志查询', 'infra:api-access-log:query', 3, 1, 1078, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2021-03-10 01:28:04', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1089, '日志查询', 'infra:api-error-log:query', 3, 1, 1084, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2021-03-10 01:29:09', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1090, '文件列表', '', 2, 5, 1243, 'file', 'ep:upload-filled', 'infra/file/index', 'InfraFile', 0, b'1', b'1', b'1', '', '2021-03-12 20:16:20', '1', '2024-02-29 08:53:02', b'0');
-INSERT INTO `system_menu` VALUES (1091, '文件查询', 'infra:file:query', 3, 1, 1090, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-03-12 20:16:20', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1092, '文件删除', 'infra:file:delete', 3, 4, 1090, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-03-12 20:16:20', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1118, '请假查询', '', 2, 0, 5, 'leave', 'fa:leanpub', 'bpm/oa/leave/index', 'BpmOALeave', 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2024-02-29 12:38:21', b'0');
-INSERT INTO `system_menu` VALUES (1119, '请假申请查询', 'bpm:oa-leave:query', 3, 1, 1118, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1120, '请假申请创建', 'bpm:oa-leave:create', 3, 2, 1118, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1185, '工作流程', '', 1, 50, 0, '/bpm', 'fa:medium', NULL, NULL, 0, b'1', b'1', b'1', '1', '2021-12-30 20:26:36', '1', '2024-02-29 12:43:43', b'0');
-INSERT INTO `system_menu` VALUES (1186, '流程管理', '', 1, 10, 1185, 'manager', 'fa:dedent', NULL, NULL, 0, b'1', b'1', b'1', '1', '2021-12-30 20:28:30', '1', '2024-02-29 12:36:02', b'0');
-INSERT INTO `system_menu` VALUES (1187, '流程表单', '', 2, 2, 1186, 'form', 'fa:hdd-o', 'bpm/form/index', 'BpmForm', 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2024-03-19 12:25:25', b'0');
-INSERT INTO `system_menu` VALUES (1188, '表单查询', 'bpm:form:query', 3, 1, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1189, '表单创建', 'bpm:form:create', 3, 2, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1190, '表单更新', 'bpm:form:update', 3, 3, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1191, '表单删除', 'bpm:form:delete', 3, 4, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1192, '表单导出', 'bpm:form:export', 3, 5, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1193, '流程模型', '', 2, 1, 1186, 'model', 'fa-solid:project-diagram', 'bpm/model/index', 'BpmModel', 0, b'1', b'1', b'1', '1', '2021-12-31 23:24:58', '1', '2024-03-19 12:25:19', b'0');
-INSERT INTO `system_menu` VALUES (1194, '模型查询', 'bpm:model:query', 3, 1, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:10', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1195, '模型创建', 'bpm:model:create', 3, 2, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:24', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1197, '模型更新', 'bpm:model:update', 3, 4, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:28', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1198, '模型删除', 'bpm:model:delete', 3, 5, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:43', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1199, '模型发布', 'bpm:model:deploy', 3, 6, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:03:24', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1200, '审批中心', '', 2, 20, 1185, 'task', 'fa:tasks', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-01-07 23:51:48', '1', '2024-03-21 00:33:15', b'0');
-INSERT INTO `system_menu` VALUES (1201, '我的流程', '', 2, 1, 1200, 'my', 'fa-solid:book', 'bpm/processInstance/index', 'BpmProcessInstanceMy', 0, b'1', b'1', b'1', '', '2022-01-07 15:53:44', '1', '2024-03-21 23:52:12', b'0');
-INSERT INTO `system_menu` VALUES (1202, '流程实例的查询', 'bpm:process-instance:query', 3, 1, 1201, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-07 15:53:44', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1207, '待办任务', '', 2, 10, 1200, 'todo', 'fa:slack', 'bpm/task/todo/index', 'BpmTodoTask', 0, b'1', b'1', b'1', '1', '2022-01-08 10:33:37', '1', '2024-02-29 12:37:39', b'0');
-INSERT INTO `system_menu` VALUES (1208, '已办任务', '', 2, 20, 1200, 'done', 'fa:delicious', 'bpm/task/done/index', 'BpmDoneTask', 0, b'1', b'1', b'1', '1', '2022-01-08 10:34:13', '1', '2024-02-29 12:37:54', b'0');
-INSERT INTO `system_menu` VALUES (1209, '用户分组', '', 2, 4, 1186, 'user-group', 'fa:user-secret', 'bpm/group/index', 'BpmUserGroup', 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '1', '2024-03-21 23:55:29', b'0');
-INSERT INTO `system_menu` VALUES (1210, '用户组查询', 'bpm:user-group:query', 3, 1, 1209, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1211, '用户组创建', 'bpm:user-group:create', 3, 2, 1209, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1212, '用户组更新', 'bpm:user-group:update', 3, 3, 1209, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1213, '用户组删除', 'bpm:user-group:delete', 3, 4, 1209, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1215, '流程定义查询', 'bpm:process-definition:query', 3, 10, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:21:43', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1216, '流程任务分配规则查询', 'bpm:task-assign-rule:query', 3, 20, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:26:53', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1217, '流程任务分配规则创建', 'bpm:task-assign-rule:create', 3, 21, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:28:15', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1218, '流程任务分配规则更新', 'bpm:task-assign-rule:update', 3, 22, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:28:41', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1219, '流程实例的创建', 'bpm:process-instance:create', 3, 2, 1201, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:36:15', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1220, '流程实例的取消', 'bpm:process-instance:cancel', 3, 3, 1201, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:36:33', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1221, '流程任务的查询', 'bpm:task:query', 3, 1, 1207, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:38:52', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1222, '流程任务的更新', 'bpm:task:update', 3, 2, 1207, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:39:24', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1237, '文件配置', '', 2, 0, 1243, 'file-config', 'fa-solid:file-signature', 'infra/fileConfig/index', 'InfraFileConfig', 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '1', '2024-02-29 08:52:54', b'0');
-INSERT INTO `system_menu` VALUES (1238, '文件配置查询', 'infra:file-config:query', 3, 1, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1239, '文件配置创建', 'infra:file-config:create', 3, 2, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1240, '文件配置更新', 'infra:file-config:update', 3, 3, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1241, '文件配置删除', 'infra:file-config:delete', 3, 4, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1242, '文件配置导出', 'infra:file-config:export', 3, 5, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` VALUES (1243, '文件管理', '', 2, 6, 2, 'file', 'ep:files', NULL, '', 0, b'1', b'1', b'1', '1', '2022-03-16 23:47:40', '1', '2024-04-23 00:02:11', b'0');
-INSERT INTO `system_menu` VALUES (1261, 'OAuth 2.0', '', 2, 10, 1, 'oauth2', 'fa:dashcube', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-05-09 23:38:17', '1', '2024-02-29 01:12:08', b'0');
-INSERT INTO `system_menu` VALUES (1263, '应用管理', '', 2, 0, 1261, 'oauth2/application', 'fa:hdd-o', 'system/oauth2/client/index', 'SystemOAuth2Client', 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2024-02-29 01:13:14', b'0');
-INSERT INTO `system_menu` VALUES (1264, '客户端查询', 'system:oauth2-client:query', 3, 1, 1263, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:06', b'0');
-INSERT INTO `system_menu` VALUES (1265, '客户端创建', 'system:oauth2-client:create', 3, 2, 1263, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:23', b'0');
-INSERT INTO `system_menu` VALUES (1266, '客户端更新', 'system:oauth2-client:update', 3, 3, 1263, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:28', b'0');
-INSERT INTO `system_menu` VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0');
-INSERT INTO `system_menu` VALUES (2083, '地区管理', '', 2, 14, 1, 'area', 'fa:map-marker', 'system/area/index', 'SystemArea', 0, b'1', b'1', b'1', '1', '2022-12-23 17:35:05', '1', '2024-02-29 08:50:28', b'0');
-INSERT INTO `system_menu` VALUES (2472, '主子表(内嵌)', '', 2, 12, 1070, 'demo03-inner', 'fa:power-off', 'infra/demo/demo03/inner/index', 'Demo03StudentInner', 0, b'1', b'1', b'1', '', '2023-11-13 04:39:51', '1', '2023-11-16 23:53:46', b'0');
-INSERT INTO `system_menu` VALUES (2478, '单表(增删改查)', '', 2, 1, 1070, 'demo01-contact', 'ep:bicycle', 'infra/demo/demo01/index', 'Demo01Contact', 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '1', '2023-11-16 20:34:40', b'0');
-INSERT INTO `system_menu` VALUES (2479, '示例联系人查询', 'infra:demo01-contact:query', 3, 1, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
-INSERT INTO `system_menu` VALUES (2480, '示例联系人创建', 'infra:demo01-contact:create', 3, 2, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
-INSERT INTO `system_menu` VALUES (2481, '示例联系人更新', 'infra:demo01-contact:update', 3, 3, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
-INSERT INTO `system_menu` VALUES (2482, '示例联系人删除', 'infra:demo01-contact:delete', 3, 4, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
-INSERT INTO `system_menu` VALUES (2483, '示例联系人导出', 'infra:demo01-contact:export', 3, 5, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
-INSERT INTO `system_menu` VALUES (2484, '树表(增删改查)', '', 2, 2, 1070, 'demo02-category', 'fa:tree', 'infra/demo/demo02/index', 'Demo02Category', 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '1', '2023-11-16 20:35:01', b'0');
-INSERT INTO `system_menu` VALUES (2485, '示例分类查询', 'infra:demo02-category:query', 3, 1, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
-INSERT INTO `system_menu` VALUES (2486, '示例分类创建', 'infra:demo02-category:create', 3, 2, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
-INSERT INTO `system_menu` VALUES (2487, '示例分类更新', 'infra:demo02-category:update', 3, 3, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
-INSERT INTO `system_menu` VALUES (2488, '示例分类删除', 'infra:demo02-category:delete', 3, 4, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
-INSERT INTO `system_menu` VALUES (2489, '示例分类导出', 'infra:demo02-category:export', 3, 5, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
-INSERT INTO `system_menu` VALUES (2490, '主子表(标准)', '', 2, 10, 1070, 'demo03-normal', 'fa:battery-3', 'infra/demo/demo03/normal/index', 'Demo03StudentNormal', 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '1', '2023-11-16 23:10:03', b'0');
-INSERT INTO `system_menu` VALUES (2491, '学生查询', 'infra:demo03-student:query', 3, 1, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
-INSERT INTO `system_menu` VALUES (2492, '学生创建', 'infra:demo03-student:create', 3, 2, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
-INSERT INTO `system_menu` VALUES (2493, '学生更新', 'infra:demo03-student:update', 3, 3, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
-INSERT INTO `system_menu` VALUES (2494, '学生删除', 'infra:demo03-student:delete', 3, 4, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
-INSERT INTO `system_menu` VALUES (2495, '学生导出', 'infra:demo03-student:export', 3, 5, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
-INSERT INTO `system_menu` VALUES (2497, '主子表(ERP)', '', 2, 11, 1070, 'demo03-erp', 'ep:calendar', 'infra/demo/demo03/erp/index', 'Demo03StudentERP', 0, b'1', b'1', b'1', '', '2023-11-16 15:50:59', '1', '2023-11-17 13:19:56', b'0');
-INSERT INTO `system_menu` VALUES (2525, 'WebSocket', '', 2, 5, 2, 'websocket', 'ep:connection', 'infra/webSocket/index', 'InfraWebSocket', 0, b'1', b'1', b'1', '1', '2023-11-23 19:41:55', '1', '2024-04-23 00:02:00', b'0');
-INSERT INTO `system_menu` VALUES (2713, '抄送我的', 'bpm:process-instance-cc:query', 2, 30, 1200, 'copy', 'ep:copy-document', 'bpm/task/copy/index', 'BpmProcessInstanceCopy', 0, b'1', b'1', b'1', '1', '2024-03-17 21:50:23', '1', '2024-04-24 19:55:12', b'0');
-INSERT INTO `system_menu` VALUES (2714, '流程分类', '', 2, 3, 1186, 'category', 'fa:object-ungroup', 'bpm/category/index', 'BpmCategory', 0, b'1', b'1', b'1', '', '2024-03-08 02:00:51', '1', '2024-03-21 23:51:18', b'0');
-INSERT INTO `system_menu` VALUES (2715, '分类查询', 'bpm:category:query', 3, 1, 2714, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-03-08 02:00:51', '1', '2024-03-19 14:36:25', b'0');
-INSERT INTO `system_menu` VALUES (2716, '分类创建', 'bpm:category:create', 3, 2, 2714, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-03-08 02:00:51', '1', '2024-03-19 14:36:31', b'0');
-INSERT INTO `system_menu` VALUES (2717, '分类更新', 'bpm:category:update', 3, 3, 2714, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-03-08 02:00:51', '1', '2024-03-19 14:36:35', b'0');
-INSERT INTO `system_menu` VALUES (2718, '分类删除', 'bpm:category:delete', 3, 4, 2714, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-03-08 02:00:51', '1', '2024-03-19 14:36:41', b'0');
-INSERT INTO `system_menu` VALUES (2720, '发起流程', '', 2, 0, 1200, 'create', 'fa-solid:grin-stars', 'bpm/processInstance/create/index', 'BpmProcessInstanceCreate', 0, b'1', b'0', b'1', '1', '2024-03-19 19:46:05', '1', '2024-03-23 19:03:42', b'0');
-INSERT INTO `system_menu` VALUES (2721, '流程实例', '', 2, 10, 1186, 'process-instance/manager', 'fa:square', 'bpm/processInstance/manager/index', 'BpmProcessInstanceManager', 0, b'1', b'1', b'1', '1', '2024-03-21 23:57:30', '1', '2024-03-21 23:57:30', b'0');
-INSERT INTO `system_menu` VALUES (2722, '流程实例的查询(管理员)', 'bpm:process-instance:manager-query', 3, 1, 2721, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:18:27', '1', '2024-03-22 08:19:05', b'0');
-INSERT INTO `system_menu` VALUES (2723, '流程实例的取消(管理员)', 'bpm:process-instance:cancel-by-admin', 3, 2, 2721, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:19:25', '1', '2024-03-22 08:19:25', b'0');
-INSERT INTO `system_menu` VALUES (2724, '流程任务', '', 2, 11, 1186, 'process-tasnk', 'ep:collection-tag', 'bpm/task/manager/index', 'BpmManagerTask', 0, b'1', b'1', b'1', '1', '2024-03-22 08:43:22', '1', '2024-03-22 08:43:27', b'0');
-INSERT INTO `system_menu` VALUES (2725, '流程任务的查询(管理员)', 'bpm:task:mananger-query', 3, 1, 2724, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:43:49', '1', '2024-03-22 08:43:49', b'0');
-INSERT INTO `system_menu` VALUES (2726, '流程监听器', '', 2, 5, 1186, 'process-listener', 'fa:assistive-listening-systems', 'bpm/processListener/index', 'BpmProcessListener', 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '1', '2024-03-23 13:13:38', b'0');
-INSERT INTO `system_menu` VALUES (2727, '流程监听器查询', 'bpm:process-listener:query', 3, 1, 2726, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '', '2024-03-09 16:05:34', b'0');
-INSERT INTO `system_menu` VALUES (2728, '流程监听器创建', 'bpm:process-listener:create', 3, 2, 2726, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '', '2024-03-09 16:05:34', b'0');
-INSERT INTO `system_menu` VALUES (2729, '流程监听器更新', 'bpm:process-listener:update', 3, 3, 2726, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '', '2024-03-09 16:05:34', b'0');
-INSERT INTO `system_menu` VALUES (2730, '流程监听器删除', 'bpm:process-listener:delete', 3, 4, 2726, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '', '2024-03-09 16:05:34', b'0');
-INSERT INTO `system_menu` VALUES (2731, '流程表达式', '', 2, 6, 1186, 'process-expression', 'fa:wpexplorer', 'bpm/processExpression/index', 'BpmProcessExpression', 0, b'1', b'1', b'1', '', '2024-03-09 22:35:08', '1', '2024-03-23 19:43:05', b'0');
-INSERT INTO `system_menu` VALUES (2732, '流程表达式查询', 'bpm:process-expression:query', 3, 1, 2731, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 22:35:08', '', '2024-03-09 22:35:08', b'0');
-INSERT INTO `system_menu` VALUES (2733, '流程表达式创建', 'bpm:process-expression:create', 3, 2, 2731, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 22:35:08', '', '2024-03-09 22:35:08', b'0');
-INSERT INTO `system_menu` VALUES (2734, '流程表达式更新', 'bpm:process-expression:update', 3, 3, 2731, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 22:35:08', '', '2024-03-09 22:35:08', b'0');
-INSERT INTO `system_menu` VALUES (2735, '流程表达式删除', 'bpm:process-expression:delete', 3, 4, 2731, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 22:35:08', '', '2024-03-09 22:35:08', b'0');
-INSERT INTO `system_menu` VALUES (2740, '监控中心', '', 1, 10, 2, 'monitors', 'ep:monitor', '', '', 0, b'1', b'1', b'1', '1', '2024-04-23 00:04:44', '1', '2024-04-23 00:04:44', b'0');
-INSERT INTO `system_menu` VALUES (2913, '流程清理', 'bpm:model:clean', 3, 7, 1193, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-17 19:32:06', '1', '2025-01-17 19:32:06', b'0');
-INSERT INTO `system_menu` VALUES (900000, '系统管理', '', 1, 10, 0, '/system', 'ep:tools', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900001, '用户管理', 'system:user:list', 2, 1, 900000, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900002, '角色管理', '', 2, 2, 900000, 'role', 'ep:user', 'system/role/index', 'SystemRole', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900003, '菜单管理', '', 2, 3, 900000, 'menu', 'ep:menu', 'system/menu/index', 'SystemMenu', 0, b'1', b'1', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900004, '模板参考', '', 1, 900, 0, '/template-reference', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900005, '异常页', '', 1, 7, 900004, '/exception', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900006, '403', '', 2, 0, 900005, '/exception/403', 'mdi:file-document-multiple-outline', 'exception/403/index', 'Exception403', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900007, '404', '', 2, 0, 900005, '/exception/404', 'mdi:file-document-multiple-outline', 'exception/404/index', 'Exception404', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900008, '500', '', 2, 0, 900005, '/exception/500', 'mdi:file-document-multiple-outline', 'exception/500/index', 'Exception500', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900009, '文档', '', 1, 2, 900004, '/document', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900010, 'Ant Design Vue文档', '', 2, 7, 900009, '/document/antd', 'mdi:file-document-multiple-outline', 'document/antd/index', 'DocumentAntd', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900011, 'Naive UI文档', '', 2, 6, 900009, '/document/naive', 'mdi:file-document-multiple-outline', 'document/naive/index', 'DocumentNaive', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900012, 'Element Plus文档', '', 2, 7, 900009, '/document/element-plus', 'mdi:file-document-multiple-outline', 'document/element-plus/index', 'DocumentElementPlus', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900013, 'Alova文档', '', 2, 8, 900009, '/document/alova', 'mdi:file-document-multiple-outline', 'document/alova/index', 'DocumentAlova', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900014, 'UnoCSS文档', '', 2, 5, 900009, '/document/unocss', 'mdi:file-document-multiple-outline', 'document/unocss/index', 'DocumentUnocss', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900015, 'Vite文档', '', 2, 4, 900009, '/document/vite', 'mdi:file-document-multiple-outline', 'document/vite/index', 'DocumentVite', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900016, 'Vue文档', '', 2, 3, 900009, '/document/vue', 'mdi:file-document-multiple-outline', 'document/vue/index', 'DocumentVue', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900017, '关于', '', 2, 10, 900004, '/about', 'mdi:file-document-multiple-outline', 'about/index', 'About', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900018, 'alova示例', '', 1, 7, 900004, '/alova', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900019, 'alova请求', '', 2, 1, 900018, '/alova/request', 'mdi:file-document-multiple-outline', 'alova/request/index', 'AlovaRequest', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900020, '场景化请求', '', 2, 3, 900018, '/alova/scenes', 'mdi:file-document-multiple-outline', 'alova/scenes/index', 'AlovaScenes', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900021, '用户列表', '', 2, 2, 900018, '/alova/user', 'mdi:file-document-multiple-outline', 'alova/user/index', 'AlovaUser', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900022, '系统功能', '', 1, 6, 900004, '/function', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900023, '请求', '', 2, 3, 900022, '/function/request', 'mdi:file-document-multiple-outline', 'function/request/index', 'FunctionRequest', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900024, '超级管理员可见', '', 2, 5, 900022, '/function/super-page', 'mdi:file-document-multiple-outline', 'function/super-page/index', 'FunctionSuperPage', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900025, '标签页', '', 2, 1, 900022, '/function/tab', 'mdi:file-document-multiple-outline', 'function/tab/index', 'FunctionTab', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900026, '切换权限', '', 2, 4, 900022, '/function/toggle-auth', 'mdi:file-document-multiple-outline', 'function/toggle-auth/index', 'FunctionToggleAuth', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900027, '首页', '', 2, 1, 900004, '/home', 'mdi:file-document-multiple-outline', 'home/index', 'Home', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900028, '多级菜单', '', 1, 8, 900004, '/multi-menu', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900029, '菜单一', '', 1, 1, 900028, '/multi-menu/first', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900030, '菜单一子菜单', '', 2, 0, 900029, '/multi-menu/first/child', 'mdi:file-document-multiple-outline', 'multi-menu/first/child/index', 'MultiMenuFirstChild', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900031, '菜单二', '', 1, 2, 900028, '/multi-menu/second', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900032, '菜单二子菜单', '', 1, 0, 900031, '/multi-menu/second/child', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900033, '菜单二子菜单首页', '', 2, 0, 900032, '/multi-menu/second/child/home', 'mdi:file-document-multiple-outline', 'multi-menu/second/child/home/index', 'MultiMenuSecondChildHome', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:02', 'admin', '2026-03-18 13:42:02', b'0');
-INSERT INTO `system_menu` VALUES (900034, '插件示例', '', 1, 7, 900004, '/plugin', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900035, '条形码', '', 2, 0, 900034, '/plugin/barcode', 'mdi:file-document-multiple-outline', 'plugin/barcode/index', 'PluginBarcode', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900036, '图表', '', 1, 0, 900034, '/plugin/charts', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900037, 'AntV', '', 2, 0, 900036, '/plugin/charts/antv', 'mdi:file-document-multiple-outline', 'plugin/charts/antv/index', 'PluginChartsAntv', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900038, 'ECharts', '', 2, 0, 900036, '/plugin/charts/echarts', 'mdi:file-document-multiple-outline', 'plugin/charts/echarts/index', 'PluginChartsEcharts', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900039, 'VChart', '', 2, 0, 900036, '/plugin/charts/vchart', 'mdi:file-document-multiple-outline', 'plugin/charts/vchart/index', 'PluginChartsVchart', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900040, '剪贴板', '', 2, 0, 900034, '/plugin/copy', 'mdi:file-document-multiple-outline', 'plugin/copy/index', 'PluginCopy', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900041, '编辑器', '', 1, 0, 900034, '/plugin/editor', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900042, 'MD 编辑器', '', 2, 0, 900041, '/plugin/editor/markdown', 'mdi:file-document-multiple-outline', 'plugin/editor/markdown/index', 'PluginEditorMarkdown', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900043, '富文本编辑器', '', 2, 0, 900041, '/plugin/editor/quill', 'mdi:file-document-multiple-outline', 'plugin/editor/quill/index', 'PluginEditorQuill', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900044, 'Excel', '', 2, 0, 900034, '/plugin/excel', 'mdi:file-document-multiple-outline', 'plugin/excel/index', 'PluginExcel', 0, b'1', b'1', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900045, '甘特图', '', 1, 0, 900034, '/plugin/gantt', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900046, 'dhtmlxGantt', '', 2, 0, 900045, '/plugin/gantt/dhtmlx', 'mdi:file-document-multiple-outline', 'plugin/gantt/dhtmlx/index', 'PluginGanttDhtmlx', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900047, 'VTableGantt', '', 2, 0, 900045, '/plugin/gantt/vtable', 'mdi:file-document-multiple-outline', 'plugin/gantt/vtable/index', 'PluginGanttVtable', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900048, '图标', '', 2, 0, 900034, '/plugin/icon', 'mdi:file-document-multiple-outline', 'plugin/icon/index', 'PluginIcon', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900049, '地图', '', 2, 0, 900034, '/plugin/map', 'mdi:file-document-multiple-outline', 'plugin/map/index', 'PluginMap', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900050, 'PDF 预览', '', 2, 0, 900034, '/plugin/pdf', 'mdi:file-document-multiple-outline', 'plugin/pdf/index', 'PluginPdf', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900051, '拼音', '', 2, 0, 900034, '/plugin/pinyin', 'mdi:file-document-multiple-outline', 'plugin/pinyin/index', 'PluginPinyin', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900052, '打印', '', 2, 0, 900034, '/plugin/print', 'mdi:file-document-multiple-outline', 'plugin/print/index', 'PluginPrint', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900053, 'Swiper', '', 2, 0, 900034, '/plugin/swiper', 'mdi:file-document-multiple-outline', 'plugin/swiper/index', 'PluginSwiper', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900054, '表格', '', 1, 0, 900034, '/plugin/tables', 'mdi:file-document-multiple-outline', NULL, NULL, 0, b'1', b'0', b'1', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900055, 'VTable', '', 2, 0, 900054, '/plugin/tables/vtable', 'mdi:file-document-multiple-outline', 'plugin/tables/vtable/index', 'PluginTablesVtable', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900056, '打字机', '', 2, 0, 900034, '/plugin/typeit', 'mdi:file-document-multiple-outline', 'plugin/typeit/index', 'PluginTypeit', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_menu` VALUES (900057, '视频', '', 2, 0, 900034, '/plugin/video', 'mdi:file-document-multiple-outline', 'plugin/video/index', 'PluginVideo', 0, b'1', b'0', b'0', 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-
--- ----------------------------
--- Table structure for system_post
--- ----------------------------
-DROP TABLE IF EXISTS `system_post`;
-CREATE TABLE `system_post` (
- `id` bigint NOT NULL COMMENT '岗位ID',
- `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '岗位编码',
- `post_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '岗位类型:management/technical/business',
- `level_rank` int NULL DEFAULT NULL COMMENT '岗位等级排序,越大级别越高',
- `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '岗位名称',
- `sort` int NOT NULL COMMENT '显示顺序',
- `status` tinyint NOT NULL 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,
- INDEX `idx_system_post_type`(`post_type` ASC) USING BTREE,
- INDEX `idx_system_post_level_rank`(`level_rank` ASC) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_post
--- ----------------------------
-INSERT INTO `system_post` VALUES (2, 'se', NULL, NULL, '项目经理', 2, 0, '', 'admin', '2021-01-05 17:03:48', '1', '2023-11-15 09:18:20', b'0');
-
--- ----------------------------
--- Table structure for system_role
--- ----------------------------
-DROP TABLE IF EXISTS `system_role`;
-CREATE TABLE `system_role` (
- `id` bigint NOT NULL COMMENT '角色ID',
- `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称',
- `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色权限字符串',
- `sort` int NOT NULL COMMENT '显示顺序',
- `data_scope` tinyint NOT NULL DEFAULT 1 COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
- `data_scope_dept_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '数据范围(指定部门数组)',
- `status` tinyint NOT NULL COMMENT '角色状态(0正常 1停用)',
- `type` tinyint NOT 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
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_role
--- ----------------------------
-INSERT INTO `system_role` VALUES (1, '超级管理员', 'super_admin', 1, 1, '', 0, 1, '超级管理员', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:21', b'0');
-INSERT INTO `system_role` VALUES (2, '普通角色', 'common', 2, 2, '', 0, 1, '普通角色', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:20', b'0');
-
--- ----------------------------
--- Table structure for system_role_menu
--- ----------------------------
-DROP TABLE IF EXISTS `system_role_menu`;
-CREATE TABLE `system_role_menu` (
- `id` bigint NOT NULL COMMENT '自增编号',
- `role_id` bigint NOT NULL COMMENT '角色ID',
- `menu_id` bigint NOT NULL COMMENT '菜单ID',
- `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
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_role_menu
--- ----------------------------
-INSERT INTO `system_role_menu` VALUES (434, 2, 1, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (477, 2, 100, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (478, 2, 101, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (479, 2, 102, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (481, 2, 103, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (483, 2, 104, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (485, 2, 105, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (490, 2, 108, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (492, 2, 109, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (541, 2, 500, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (543, 2, 501, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0');
-INSERT INTO `system_role_menu` VALUES (675, 2, 2, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (689, 2, 1077, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (690, 2, 1078, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (692, 2, 1083, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (693, 2, 1084, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (699, 2, 1090, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (703, 2, 106, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (705, 2, 111, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (706, 2, 112, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (707, 2, 113, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0');
-INSERT INTO `system_role_menu` VALUES (1991, 2, 1024, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (1992, 2, 1025, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (1993, 2, 1026, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (1994, 2, 1027, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (1995, 2, 1028, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (1996, 2, 1029, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (1997, 2, 1030, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (1998, 2, 1031, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (1999, 2, 1032, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2000, 2, 1033, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2001, 2, 1034, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2002, 2, 1035, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2007, 2, 1040, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2008, 2, 1042, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2009, 2, 1043, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2010, 2, 1045, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2011, 2, 1046, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2012, 2, 1048, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2021, 2, 2083, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2024, 2, 1063, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2025, 2, 1064, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2026, 2, 1065, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2027, 2, 1066, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2028, 2, 1067, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2029, 2, 1070, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2036, 2, 1082, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2037, 2, 1085, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2038, 2, 1086, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2040, 2, 1088, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2041, 2, 1089, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2042, 2, 1091, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2043, 2, 1092, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2072, 2, 114, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2076, 2, 116, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2103, 2, 1237, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2104, 2, 1238, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2105, 2, 1239, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2106, 2, 1240, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2107, 2, 1241, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2108, 2, 1242, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2109, 2, 1243, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2123, 2, 1261, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2124, 2, 1263, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2125, 2, 1264, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2126, 2, 1265, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2127, 2, 1266, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2128, 2, 1267, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2129, 2, 1001, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2130, 2, 1002, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2131, 2, 1003, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2132, 2, 1004, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2133, 2, 1005, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2134, 2, 1006, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2135, 2, 1007, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2136, 2, 1008, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2137, 2, 1009, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2138, 2, 1010, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2139, 2, 1011, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2140, 2, 1012, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2141, 2, 1013, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2143, 2, 1015, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2145, 2, 1017, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2146, 2, 1018, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2147, 2, 1019, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2148, 2, 1020, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2149, 2, 1021, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2150, 2, 1022, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (2151, 2, 1023, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0');
-INSERT INTO `system_role_menu` VALUES (5780, 2, 2740, '1', '2024-07-07 20:39:38', '1', '2024-07-07 20:39:38', b'0');
-INSERT INTO `system_role_menu` VALUES (990000, 1, 900000, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990001, 1, 900001, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990002, 1, 900002, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990003, 1, 900003, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990004, 1, 900004, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990005, 1, 900005, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990006, 1, 900006, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990007, 1, 900007, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990008, 1, 900008, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990009, 1, 900009, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990010, 1, 900010, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990011, 1, 900011, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990012, 1, 900012, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990013, 1, 900013, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990014, 1, 900014, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990015, 1, 900015, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990016, 1, 900016, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990017, 1, 900017, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990018, 1, 900018, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990019, 1, 900019, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990020, 1, 900020, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990021, 1, 900021, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990022, 1, 900022, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990023, 1, 900023, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990024, 1, 900024, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990025, 1, 900025, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990026, 1, 900026, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990027, 1, 900027, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990028, 1, 900028, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990029, 1, 900029, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990030, 1, 900030, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990031, 1, 900031, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990032, 1, 900032, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990033, 1, 900033, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990034, 1, 900034, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990035, 1, 900035, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990036, 1, 900036, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990037, 1, 900037, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990038, 1, 900038, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990039, 1, 900039, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990040, 1, 900040, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990041, 1, 900041, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990042, 1, 900042, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990043, 1, 900043, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990044, 1, 900044, 'admin', '2026-03-18 13:42:03', 'admin', '2026-03-18 13:42:03', b'0');
-INSERT INTO `system_role_menu` VALUES (990045, 1, 900045, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990046, 1, 900046, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990047, 1, 900047, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990048, 1, 900048, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990049, 1, 900049, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990050, 1, 900050, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990051, 1, 900051, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990052, 1, 900052, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990053, 1, 900053, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990054, 1, 900054, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990055, 1, 900055, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990056, 1, 900056, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-INSERT INTO `system_role_menu` VALUES (990057, 1, 900057, 'admin', '2026-03-18 13:42:04', 'admin', '2026-03-18 13:42:04', b'0');
-
--- ----------------------------
--- Table structure for system_user_post
--- ----------------------------
-DROP TABLE IF EXISTS `system_user_post`;
-CREATE TABLE `system_user_post` (
- `id` bigint NOT NULL COMMENT 'id',
- `user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户ID',
- `post_id` bigint NOT NULL DEFAULT 0 COMMENT '岗位ID',
- `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
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户岗位表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_user_post
--- ----------------------------
-INSERT INTO `system_user_post` VALUES (112, 1, 1, 'admin', '2022-05-02 07:25:24', 'admin', '2022-05-02 07:25:24', b'0');
-INSERT INTO `system_user_post` VALUES (125, 1, 2, '1', '2024-07-13 22:31:39', '1', '2024-07-13 22:31:39', b'0');
-
--- ----------------------------
--- Table structure for system_user_role
--- ----------------------------
-DROP TABLE IF EXISTS `system_user_role`;
-CREATE TABLE `system_user_role` (
- `id` bigint NOT NULL COMMENT '自增编号',
- `user_id` bigint NOT NULL COMMENT '用户ID',
- `role_id` bigint NOT NULL COMMENT '角色ID',
- `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
- `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
- `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- `deleted` bit(1) NULL DEFAULT b'0' COMMENT '是否删除',
- PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_user_role
--- ----------------------------
-INSERT INTO `system_user_role` VALUES (1, 1, 1, '', '2022-01-11 13:19:45', '', '2022-05-12 12:35:17', b'0');
-INSERT INTO `system_user_role` VALUES (18, 1, 2, '1', '2022-05-12 20:39:29', '1', '2022-05-12 20:39:29', b'0');
-
--- ----------------------------
--- Table structure for system_user_visibility_config
--- ----------------------------
-DROP TABLE IF EXISTS `system_user_visibility_config`;
-CREATE TABLE `system_user_visibility_config` (
- `id` bigint NOT NULL COMMENT '主键ID',
- `user_id` bigint NOT NULL COMMENT '用户ID',
- `visibility_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '可见范围类型:all/directions/projects',
- `visible_direction_ids` json NULL COMMENT '补充可见方向ID集合',
- `visible_project_ids` json NULL COMMENT '补充可见项目ID集合',
- `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_visibility_user`(`user_id` ASC) USING BTREE,
- INDEX `idx_visibility_type`(`visibility_type` ASC) USING BTREE,
- CONSTRAINT `chk_visibility_type` CHECK (`visibility_type` in (_utf8mb4'all',_utf8mb4'directions',_utf8mb4'projects'))
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户补充可见范围配置表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_user_visibility_config
--- ----------------------------
-
--- ----------------------------
--- Table structure for system_users
--- ----------------------------
-DROP TABLE IF EXISTS `system_users`;
-CREATE TABLE `system_users` (
- `id` bigint NOT NULL COMMENT '用户ID',
- `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户账号',
- `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '密码',
- `nickname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称',
- `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
- `dept_id` bigint NULL DEFAULT NULL COMMENT '部门ID',
- `position_id` bigint NULL DEFAULT NULL COMMENT '主岗位ID',
- `resigned_at` datetime NULL DEFAULT NULL COMMENT '离职时间',
- `post_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '岗位编号数组',
- `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '用户邮箱',
- `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '手机号码',
- `sex` tinyint NULL DEFAULT 0 COMMENT '用户性别',
- `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '头像地址',
- `status` tinyint NOT NULL DEFAULT 0 COMMENT '帐号状态(0正常 1停用)',
- `login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '最后登录IP',
- `login_date` datetime 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_system_users_dept_id`(`dept_id` ASC) USING BTREE,
- INDEX `idx_system_users_position_id`(`position_id` ASC) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
-
--- ----------------------------
--- Records of system_users
--- ----------------------------
-INSERT INTO `system_users` VALUES (1, 'admin', '$2a$04$KljJDa/LK7QfDm0lF5OhuePhlPfjRH3tB2Wu351Uidz.oQGJXevPi', '灿能源码', '管理员', 103, 2, NULL, '[1,2]', '11aoteman@126.com', '18818260272', 2, 'http://test.rdms.iocoder.cn/20250921/avatar_1758423875594.png', 0, '192.168.2.125', '2026-03-18 16:18:02', 'admin', '2021-01-05 17:03:47', 'system', '2026-03-19 13:59:04', b'0');
-
--- ----------------------------
--- system_menu 路由扩展字段
--- ----------------------------
-ALTER TABLE `system_menu`
- ADD COLUMN `route_kind` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '路由类型:dir/view/single/iframe/external/redirect' AFTER `component_name`,
- ADD COLUMN `route_props_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '路由 props JSON' AFTER `route_kind`;
-
-UPDATE `system_menu`
-SET `component` = 'view.iframe-page',
- `component_name` = 'document_antd',
- `route_kind` = 'iframe',
- `route_props_json` = '{\"url\":\"https://www.antdv.com/\"}'
-WHERE `id` = 900010;
-
-UPDATE `system_menu`
-SET `component` = 'view.iframe-page',
- `component_name` = 'document_naive',
- `route_kind` = 'iframe',
- `route_props_json` = '{\"url\":\"https://www.naiveui.com/\"}'
-WHERE `id` = 900011;
-
-UPDATE `system_menu`
-SET `component` = 'view.iframe-page',
- `component_name` = 'document_element_plus',
- `route_kind` = 'iframe',
- `route_props_json` = '{\"url\":\"https://cn.element-plus.org/\"}'
-WHERE `id` = 900012;
-
-UPDATE `system_menu`
-SET `component` = 'view.iframe-page',
- `component_name` = 'document_alova',
- `route_kind` = 'iframe',
- `route_props_json` = '{\"url\":\"https://alova.js.org/\"}'
-WHERE `id` = 900013;
-
-UPDATE `system_menu`
-SET `component` = 'view.iframe-page',
- `component_name` = 'document_unocss',
- `route_kind` = 'iframe',
- `route_props_json` = '{\"url\":\"https://unocss.dev/\"}'
-WHERE `id` = 900014;
-
-UPDATE `system_menu`
-SET `component` = 'view.iframe-page',
- `component_name` = 'document_vite',
- `route_kind` = 'iframe',
- `route_props_json` = '{\"url\":\"https://vite.dev/\"}'
-WHERE `id` = 900015;
-
-UPDATE `system_menu`
-SET `component` = 'view.iframe-page',
- `component_name` = 'document_vue',
- `route_kind` = 'iframe',
- `route_props_json` = '{\"url\":\"https://cn.vuejs.org/\"}'
-WHERE `id` = 900016;
-
-INSERT INTO `system_menu` VALUES (1036, '组织负责人查询', 'system:org-leader:query', 3, 5, 103, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2026-03-25 00:00:00', 'admin', '2026-03-25 00:00:00', b'0');
-INSERT INTO `system_menu` VALUES (1037, '组织负责人新增', 'system:org-leader:create', 3, 6, 103, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2026-03-25 00:00:00', 'admin', '2026-03-25 00:00:00', b'0');
-INSERT INTO `system_menu` VALUES (1038, '组织负责人修改', 'system:org-leader:update', 3, 7, 103, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2026-03-25 00:00:00', 'admin', '2026-03-25 00:00:00', b'0');
-INSERT INTO `system_menu` VALUES (1039, '组织负责人删除', 'system:org-leader:delete', 3, 8, 103, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2026-03-25 00:00:00', 'admin', '2026-03-25 00:00:00', b'0');
-
-SET FOREIGN_KEY_CHECKS = 1;