From 1ef86fc1cb21926c60e35aaddd51eb6527946f61 Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Mon, 18 May 2026 21:16:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(guidelines):=20=E6=9B=B4=E6=96=B0=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=8C=87=E5=BC=95=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=88=90=E5=91=98=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 简化 AGENTS.md 内容,统一引用 CLAUDE.md 作为主要指引 - 更新 CLAUDE.md 中的工作方式和验证流程说明 - 添加产品和项目成员批量新增/移出的错误码定义 - 扩展系统角色 API 响应 DTO,增加可见性字段 - 实现产品团队成员批量新增和批量移出控制器接口 - 添加产品成员批量操作的服务层实现和业务校验逻辑 - 实现项目团队成员批量操作的相关控制器接口 - 优化产品成员列表查询,过滤不可见角色行 - 添加批量操作的审计日志记录功能 --- .claude/settings.local.json | 36 ++- AGENTS.md | 266 +----------------- CLAUDE.md | 36 ++- .../project/enums/ErrorCodeConstants.java | 11 + .../product/ProductMemberController.java | 19 ++ .../member/ProductMemberBatchCreateReqVO.java | 44 +++ .../ProductMemberBatchInactiveReqVO.java | 27 ++ .../project/ProjectMemberController.java | 19 ++ .../member/ProjectMemberBatchCreateReqVO.java | 44 +++ .../ProjectMemberBatchInactiveReqVO.java | 27 ++ .../service/product/ProductMemberService.java | 22 ++ .../product/ProductMemberServiceImpl.java | 164 +++++++++++ .../service/project/ProjectMemberService.java | 22 ++ .../project/ProjectMemberServiceImpl.java | 159 +++++++++++ .../api/permission/dto/ObjectRoleRespDTO.java | 4 + .../admin/permission/vo/role/RoleRespVO.java | 4 + .../dal/dataobject/permission/RoleDO.java | 6 + 17 files changed, 625 insertions(+), 285 deletions(-) create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/member/ProductMemberBatchCreateReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/member/ProductMemberBatchInactiveReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/member/ProjectMemberBatchCreateReqVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/member/ProjectMemberBatchInactiveReqVO.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ac21510..63e53a1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -39,7 +39,41 @@ "PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot test '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|FAILED|ERROR' | Select-Object -Last 80)", "Skill(code-review:code-review)", "Bash(Test-Path *)", - "Skill(superpowers:systematic-debugging)" + "Skill(superpowers:systematic-debugging)", + "Bash(mv \"项目框架设计与后续改造约束说明.md\" architecture/)", + "Bash(mv \"新增微服务模块必需项清单.md\" architecture/)", + "Bash(mv \"后端提交规范与示例.md\" architecture/)", + "Bash(mv \"单体启动模式可行性评估.md\" architecture/)", + "Bash(mv \"对象状态能力落地规范.md\" architecture/)", + "Bash(mv \"rdms-gateway.md\" \"modules/网关说明.md\")", + "Bash(mv \"rdms-spring-boot-starter-biz-ip.md\" \"modules/业务IP启动器.md\")", + "Bash(mv \"rdms-spring-boot-starter-env.md\" \"modules/环境配置启动器.md\")", + "Bash(mv \"rdms-spring-boot-starter-excel.md\" \"modules/Excel启动器.md\")", + "Bash(mv \"rdms-spring-boot-starter-mq.md\" \"modules/消息队列启动器.md\")", + "Bash(mv \"文件存储_内网MinIO接入说明.md\" domains/system/)", + "Bash(mv \"登录验证码前端接入说明.md\" domains/system/)", + "Bash(mv \"product/02-产品管理_业务设计.md\" domains/product/)", + "Bash(mv \"product/03-工单到任务全链路与工作流方案.md\" domains/product/)", + "Bash(mv \"product/05-产品管理_前端联调最小闭环清单.md\" domains/product/)", + "Bash(mv \"product/06-产品设置_补丁版说明.md\" domains/product/)", + "Bash(mv \"product/07-产品设置_前端联调API文档.md\" domains/product/)", + "Bash(mv \"项目/03-项目管理_业务设计.md\" domains/project/)", + "Bash(mv \"项目/2026-05-08-execution-member-change-history-design.md\" \"domains/project/2026-05-08-执行成员变更历史设计.md\")", + "Bash(mv \"项目/2026-05-09-task-fix-plan.md\" \"domains/project/2026-05-09-任务修复计划.md\")", + "Bash(mv \"项目/2026-05-09-task-logic-review.md\" \"domains/project/2026-05-09-任务逻辑评审.md\")", + "Bash(mv \"项目/2026-05-09-task-worklog-design.md\" \"domains/project/2026-05-09-任务工时设计.md\")", + "Bash(mv \"任务工时与进度模型_业内标杆调研.md\" research/)", + "Bash(mv \"任务负责人模型_业内标杆调研.md\" research/)", + "Bash(mv \"技术负债台账.html\" debt/)", + "Bash(mv \"product/product-overview-mockup.html\" \"temp/产品概览-mockup.html\")", + "Bash(mv \"项目/2026-05-12-按钮可见度演示.html\" temp/)", + "Bash(mv \"product/rdms_object_status_transition.sql\" sql/)", + "Bash(mv \"product/system_menu.sql\" sql/)", + "Bash(mv \"项目/object.sql\" sql/)", + "Bash(mv \"项目/rdms_biz_audit_log.sql\" sql/)", + "Bash(mv \"项目/sql/\"*.sql sql/)", + "Bash(mv \"项目/项目管理待确认项清单_V1.0.md\" domains/project/)", + "Bash(rmdir 项目)" ] } } diff --git a/AGENTS.md b/AGENTS.md index 754b3d3..9287846 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,267 +1,5 @@ # AGENTS.md -## 适用范围 +本仓库的 Agent 工作指引以 [`CLAUDE.md`](./CLAUDE.md) 为准。 -本说明适用于以 `C:\code\gitea\rdms\cn-rdms` 为根目录的整个仓库。 - -描述仓库现状时,以当前代码、当前配置、当前文档中可直接验证的事实为准;除非用户明确要求,不引入历史实现、过渡方案或已废弃模型来解释当前状态。 - -默认回答保持精简,优先给结论、改动点和必要风险,不做过多展开;如果存在你关心但未展开的细节,由你继续追问后再补充。 - -回答问题时不要过多代码层面的描述:默认用自然语言给结论、判断、影响面;除非用户明确要看实现细节,不要大段贴代码片段、不要逐行解读、不要把分析写成"先看 xxx.java 第 N 行"的形式。涉及代码定位时用 `file_path:line_number` 引用即可。 - -## 交互原则 - -- 默认先给执行方案,说明目标、涉及模块、预计改动点和验证方式。 -- 在用户评审并明确同意前,不直接开始实际修改、编译、测试、打包或其他执行动作。 -- 是否执行由用户决定;如果用户只要求分析、审阅或出方案,就停留在分析和方案层。 - -## 项目概览 - -这是一个面向 RDMS 服务的多模块 Maven 单仓库项目。 - -- Java 版本:17 -- 构建工具:Maven -- 根模块打包类型:`pom` -- Spring Boot 版本:`3.5.9` - -## 本地环境约定 - -- 本仓库要求使用 `JDK 17`;不要使用 `JDK 8`、`JDK 11` 或其他版本执行编译、测试、启动、打包。 -- 本机 JDK 17 路径:`C:\Program Files\Java\jdk-17` -- 如需执行 Maven、Java、测试、启动等命令,应先确认当前 `JAVA_HOME` 指向 `C:\Program Files\Java\jdk-17`,并确保 `java -version` 实际输出为 `17` -- 本机 Maven 安装路径:`C:\software\apache-maven-3.8.9` -- 如需执行 Maven 命令,优先使用完整路径:`C:\software\apache-maven-3.8.9\bin\mvn.cmd` -- 不要假设 `mvn` 已加入 PATH -- 不要假设系统默认 `JAVA_HOME` 已正确指向 JDK 17;如果当前 shell 不是 JDK 17,先在当前命令上下文显式切换后再执行 Maven -- 只有在用户已明确同意执行编译、测试、打包等 Maven 命令时,才使用上述路径执行 - -顶层模块: - -1. `rdms-system` -2. `rdms-project` -3. `rdms-framework` -4. `rdms-gateway` - -当前系统域能力主要集中在 `rdms-system`,RDMS 核心交付域能力主要集中在 `rdms-project`,但这只是现阶段结构,不应被理解为长期只保留这两个业务模块。 - -后续如果新增独立业务服务,例如项目/产品管理模块、工作流模块,应继续沿用当前仓库的模块拆分方式,而不是把所有后续业务长期堆进 `rdms-system`。 - -## 模块说明 - -### `rdms-system` - -当前已存在的系统业务聚合模块。 - -- `rdms-system/rdms-system-boot` - - 主应用模块 - - 启动入口:`rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/SystemServerApplication.java` - - 主包路径:`com.njcn.rdms.module.system` - - 常见子包:`api`、`controller`、`convert`、`dal`、`framework`、`job`、`service`、`util`、`websocket` -- `rdms-system/rdms-system-api` - - 供其他服务依赖的共享 API 模块 - - 包含对外 API 契约与枚举定义 - -说明: - -- 当前权限、用户、组织、岗位、菜单、角色等系统核心能力主要落在这里。 -- 如果后续只是给系统域补充新的系统子能力,可以继续在 `rdms-system` 内按现有结构扩展。 -- 如果后续形成独立业务域,例如 `rdms-project`、`rdms-workflow`,应优先建设为新的独立业务模块,而不是默认继续塞进 `rdms-system`。 - -### `rdms-project` - -当前已存在的 RDMS 核心交付业务聚合模块。 - -- `rdms-project/rdms-project-boot` - - 主应用模块 - - 启动入口:`rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java` - - 主包路径:`com.njcn.rdms.module.project` - - 常见子包:`api`、`controller`、`convert`、`dal`、`framework`、`service` -- `rdms-project/rdms-project-api` - - 供其他服务依赖的共享 API 模块 - - 包含对外 API 契约与枚举定义 - -说明: - -- 当前项目集、项目、产品、需求、任务、工单、执行等 RDMS 核心业务能力应优先落在这里。 -- 需要复用用户、组织、岗位、权限等系统能力时,应通过 `rdms-system-api` 调用,不要反向依赖 `rdms-system-boot`。 - -### `rdms-framework` - -共享框架与内部 starter 模块。 - -- `rdms-framework/rdms-common` - - 核心通用工具与公共抽象 -- 其他 `rdms-spring-boot-starter-*` 模块 - - 内部 starter,覆盖 `env`、`web`、`rpc`、`security`、`mybatis`、`redis`、`mq`、`websocket`、`excel`、`protection`、`test`、`biz-ip` - -### `rdms-gateway` - -Spring Cloud Gateway 网关服务。 - -- 启动入口:`rdms-gateway/src/main/java/com/njcn/rdms/gateway/GatewayServerApplication.java` -- 主包路径:`com.njcn.rdms.gateway` -- 常见子包:`filter`、`handler`、`jackson`、`route`、`util` - -## 模块演进约束 - -后续新增业务能力时,先区分下面两种情况,不要混用: - -1. 新增独立微服务模块,例如 `rdms-project`、`rdms-workflow` -2. 只是在现有 `rdms-system` 中新增一个业务子域 - -### 新增独立微服务模块 - -如果后续能力已经具备独立服务边界,应优先按下面结构建设: - -```text -rdms-xxx -├─ rdms-xxx-api -└─ rdms-xxx-boot -``` - -约束: - -- 根 `pom.xml` 增加新的聚合模块 -- `api` 模块承载对外 RPC/Feign 接口、DTO、错误码、枚举、常量 -- `boot` 模块承载启动类、controller、service、dal、convert、api 实现、模块级 framework 配置和资源文件 -- 包路径、`spring.application.name`、`ApiConstants`、`RpcConstants`、`rdms.info.base-package` 必须保持一致 -- 新服务不是简单复制 `rdms-system` 的名字,而是复用它的工程骨架和分层习惯 - -### 在 `rdms-system` 中新增业务子域 - -如果只是给系统服务补一个当前阶段仍适合放在 `rdms-system` 内的子域,则继续沿用现有结构: - -- `controller/admin/...` 或 `controller/app/...` -- `service/...` -- `dal/dataobject/...` -- `dal/mysql/...` -- `convert/...` -- 需要跨模块暴露时,在 `rdms-system-api` 中补 API、DTO、错误码、枚举 - -约束: - -- 不要为了新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层语言 -- 不要让外部模块直接依赖 `rdms-system-boot` 的 service 或 mapper -- 如果某项能力未来明显会演进成独立微服务,文档和实现上都要避免把它写死成只能存在于 `rdms-system` - -## 代码目录 - -- Java 源码:`*/src/main/java` -- 资源文件:`*/src/main/resources` -- 测试代码:`*/src/test/java` -- 本地辅助脚本:`scripts/` - -## 分层职责约束 - -### `rdms-framework` - -- `rdms-framework` 承担基础能力,不承载具体业务语义。 -- 除非出现框架级缺陷,或该能力明确属于全局可复用基础设施,否则不要把业务判断硬塞进 framework。 - -### `rdms-gateway` - -- `rdms-gateway` 只负责统一入口、令牌校验、登录用户透传、路由和网关层横切逻辑。 -- 不要在 gateway 层承载组织、成员、负责人、项目、产品、工作流状态流转或数据可见性这类业务语义。 - -### Controller 层 - -- Controller 负责 HTTP 暴露、参数校验、权限注解、结果封装。 -- 不要在 controller 中直接编排复杂业务流程,也不要直接操作多个 mapper 拼装业务规则。 -- 请求和响应对象优先沿用 `ReqVO`、`RespVO` 风格,不要直接把 DO 暴露给前端。 - -### Service 层 - -- 核心业务规则、事务、缓存、领域编排应落在 service 层。 -- 如果是已有领域增强,优先在现有 service 下扩展,不要为了“看起来更整齐”平移整套代码。 -- 不要把复杂规则散落到 controller、mapper 或 `util` 中。 - -### DAL 层 - -- 新表应有对应的 DO 和 Mapper。 -- Mapper 优先继承 `BaseMapperX`,不要重复写样板 CRUD。 -- 查询条件优先沿用 `LambdaQueryWrapperX`、默认方法封装和现有 MyBatis Plus 风格,不要无必要回退到 XML。 -- Mapper 以查询封装为主,不承担领域校验职责。 - -### Convert 层 - -- 如果某个领域已经有 `convert` 风格,则继续沿用。 -- 简单场景允许直接使用 `BeanUtils`。 -- 不要为了统一而强推所有地方都改成 MapStruct,也不要反过来把已有 convert 全部删掉。 - -## 认证与共享调用约束 - -- 默认沿用现有 OAuth2 / Token / `LoginUser` / `login-user` 透传主链,不要另造一套认证上下文体系。 -- 不要额外发明 ThreadLocal、Session 或自定义 header 体系替代当前登录态恢复方式。 -- 接口级权限判断默认沿用 `@PreAuthorize("@ss.hasPermission(...)")` 这条链路,不要绕开现有权限框架另起一套实现。 -- 跨模块、跨服务访问能力时,优先通过对应的 `*-api` 模块定义 API、DTO、常量和枚举。 -- 不要让外部模块直接依赖某个 `*-boot` 模块的 service 或 mapper。 - -## 数据与 SQL 约束 - -- 新增业务表的 DO 优先复用当前 `BaseDO` / 审计字段风格;除非表本身明确不需要逻辑删除,不要再引入另一套审计基类。 -- 不要假设运行时存在自动数据库迁移;如果代码依赖新表、新字段或新索引,必须同步补齐对应 SQL 与文档说明。 -- SQL 脚本应放在目标模块的 `src/main/resources/sql/...` 下,并保持可审阅、可单独执行、语义清晰。 -- 变更缓存、日志、审计相关逻辑时,优先沿用现有机制,不要绕开现有登录上下文、缓存约定和审计字段填充方式。 - -## 注释与编码 - -- 新增或修改代码时,关键字段、关键分支、关键约束和非直观实现应补充简洁中文注释。 -- 不要为了省事删除原有有效注释,也不要添加无信息量的注释。 -- 写入中文内容时必须保持 UTF-8 编码,并自行检查中文显示是否正常;不要用“改成英文”规避乱码问题。 -- 使用 superpowers 产出的功能文档时,例如设计文档、实施计划、联调说明,除非用户明确要求,否则默认用中文落地;代码标识、文件路径、接口路径、SQL、命令保持原始技术标识,不做意译。 - -## 工作规则 - -1. 除非任务明确要求修改共享契约或 starter,否则优先进行有边界的模块内改动,避免跨模块扩散。 -2. 业务逻辑应放在对应业务模块的 `*-boot` 实现模块;可复用契约放在对应的 `*-api` 模块;可复用框架能力放在 `rdms-framework`。 -3. 除非任务本身就是环境配置调整,否则避免修改 `application-local.yaml` 和 `application-dev.yaml`。 -4. 将本地资源 YAML 视为可能带有机器环境差异的文件;修改前先检查 git 状态。 -5. 保持既有包结构约定不变: - - 控制器放在 `controller` - - 服务层放在 `service` - - 持久层放在 `dal` - - DTO/VO 转换放在 `convert` -6. 当前系统域代码主要在 `rdms-system`,RDMS 核心交付域代码主要在 `rdms-project`,但这不是永久约束;新增业务能力时,先判断应该落在现有系统域内、现有项目交付域内,还是应建设为新的 `rdms-xxx` 业务模块。 -7. 新增共享能力时,优先扩展现有 `rdms-spring-boot-starter-*` 模块,不要在业务服务里重复堆配置。 -8. 修改跨模块使用的 API 时,需要同时更新提供方实现和对应的 `rdms-system-api` 或对应 `rdms-xxx-api` 契约。 -9. 除非用户明确要求,否则不执行任何编译、构建、测试、打包或其他会实际运行项目的命令,包括但不限于 `mvn`、启动命令和脚本。 - -## Git 操作纪律 - -### 默认不引导分支管理(**首要**) - -用户在本仓库长期固定在 `main` 上工作。开发流程中: - -- **不要主动建议建 feature 分支**(`git checkout -b feat/xxx`、`git switch -c ...`)。 -- **不要把"先切到 xxx 分支再操作"作为方案前置步骤**。 -- 一切围绕 `main` 展开:直接在 `main` 上改、`main` 上提交、`main` 上推。 -- 例外:用户明确要求建分支、或涉及多人协作 / PR 评审 / 大规模重构(此时仍只是"提一句作为可选",不强推)。 - -理由:曾出现一次"误把文件名当分支名建出怪分支 `用户行动清单.md`,后续 `git branch -D` 删分支时险些丢用户 3 天工作"的事故。事故根因就是分支管理本身——少走分支 = 少埋雷。 - -### 破坏性 git 命令必须先核实 - -任何**会丢工作**的 git 命令——`branch -D`、`reset --hard`、`clean -fd`、`push --force` / `--force-with-lease`、`checkout` / `switch` 带未提交改动、`rebase` 在已推送分支上、直接动 `.git/` 内部文件(`refs/`、`HEAD`、`packed-refs`)——**给出建议前必须先核实**,不得凭"看起来安全"就甩命令: - -1. **目标 ref 上是否有未推送 / 未合并 commit**:让用户跑 `git log --oneline -5 ` 或 `git log <主线>..` 把输出贴回来。 -2. **工作区是否干净**:`git status`。 -3. **先挂救生圈**:建议用 `git tag backup-xxx ` 锁定当前 SHA,**再执行**破坏性命令。 -4. **明示翻车回滚路径**:例如"如果不对,`git reset --hard backup-xxx` 即可回到此处"。 - -## 测试指引 - -先定义验证方式,再实施修改。默认通过以下方式验证: - -- 代码路径是否闭环,调用链是否与模块边界一致 -- 配置项、接口契约、权限标识、路由或资源注册是否前后一致 -- 改动范围是否控制在当前任务所需的最小集合内 -- 受影响的文档、SQL、配置或接口说明是否需要同步更新 - -如果任务影响了 Spring 配置、序列化、安全、路由、RPC 契约、MyBatis 行为或跨模块 API,一律明确说明哪些部分已静态检查、哪些部分尚未实际运行验证。 - -## 给后续 Agent 的说明 - -- 仓库中可能存在未提交的本地配置改动,不要覆盖与当前任务无关的编辑。 -- `docs/` 目录属于当前工作上下文的一部分,不是归档材料;做架构级修改前先查阅。 -- 根目录 `pom.xml` 负责统一版本和依赖对齐;涉及版本调整时,优先修改根 `pom.xml`,不要散落到子模块中。 +适用范围:以 `C:\code\gitea\rdms\cn-rdms` 为根目录的整个仓库。所有交互原则、本机环境、模块结构、分层职责、鉴权通道、HTTP 动词语义、数据与 SQL、注释编码、Git 纪律、验证默认动作等约束,请直接阅读 `CLAUDE.md`。 diff --git a/CLAUDE.md b/CLAUDE.md index 880075c..e46a990 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,22 +1,20 @@ # CLAUDE.md -本文件为 Claude Code 在 `C:\code\gitea\rdms\cn-rdms` 仓库工作时的常驻指引,等价并补充 `AGENTS.md`。两份文件冲突时以本文件为准;本文件未覆盖的细节回退到 `AGENTS.md`。 +本文件为 `C:\code\gitea\rdms\cn-rdms` 仓库下所有 Agent(含 Claude Code)的常驻工作指引;`AGENTS.md` 引用本文件。 ## 工作方式 -- 默认先给执行方案:目标、涉及模块、改动点、验证方式。用户明确同意前不要直接动手修改、编译、测试、打包。 +- 默认先给执行方案:目标、涉及模块、改动点、验证方式;不擅自动手。 - 用户只要分析或评审时,停在分析层;不要顺手开工。 - 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。 -- 回答保持精简,先给结论、改动点、必要风险;细节等用户追问。 -- **不要废话**:默认极简输出,不展开背景、不复述需求、不堆叠章节标题;能用一两句讲清就别写成清单;用户主动追问再展开。 -- **回答问题时不要过多代码层面的描述**:默认用自然语言给结论、判断、影响面;除非用户明确要看实现细节,不要大段贴代码片段、不要逐行解读、不要把分析写成"先看 xxx.java 第 N 行"的形式。涉及代码定位时用 `file_path:line_number` 引用即可。 +- **输出极简**:先给结论、改动点、必要风险;用自然语言给判断和影响面,少贴代码片段;涉及代码用 `file_path:line_number` 引用;用户追问再展开。 +- **下定论需要充足证据**。疑似 bug 时先判断是否稳定复现:跑了很久没动过的功能**首次**报错,优先怀疑运行时状态污染(devtools / IDE 热替换、ApplicationContext 残留、缓存、Redis / DB 连接、JVM 静态字段被旧 context 设过等),**不要凭单次堆栈就断言代码 bug,更不要直接甩修改方案**。先给"可能原因 + 最便宜的取证步骤"(多数场景是**冷重启 JVM**),用户确认能稳定复现,再讨论代码层面的修复。同款写法在仓库其它位置存在并不能反推"也是 bug",长期能跑的代码突然失效 ≠ 代码本身错。 ## 本机环境 - JDK:必须使用 `JDK 17`,路径 `C:\Program Files\Java\jdk-17`。不要使用 JDK 8 / 11 / 其他版本。 - Maven:`C:\software\apache-maven-3.8.9`,命令优先用完整路径 `C:\software\apache-maven-3.8.9\bin\mvn.cmd`。不要假设 `mvn` 在 PATH。 - 执行任何 Maven / java 命令前,先确认当前 shell 的 `JAVA_HOME` 指向 JDK 17,且 `java -version` 输出 17;否则在该命令上下文中显式切换。 -- **只有在用户已明确同意执行编译/测试/打包等命令时**,才使用上述路径执行。日常默认不跑任何会实际运行项目的命令。 ## 仓库结构 @@ -65,7 +63,7 @@ ## 认证与跨模块调用 - 默认沿用 OAuth2 / Token / `LoginUser` / `login-user` 透传主链。**不要**另造 ThreadLocal / Session / 自定义 header。 -- 跨模块/跨服务必须通过 `*-api` 模块定义契约;不要直接依赖别人的 `*-boot`。 +- 业务逻辑落 `*-boot`;可复用契约落 `*-api`;可复用框架能力落 `rdms-framework`。跨模块/跨服务必须通过 `*-api` 定义契约,**不要直接依赖别人的 `*-boot`**;改跨模块 API 时,`*-boot` 实现与对应 `*-api` 契约同步更新。 ### 鉴权:必须按"全域 / 对象域"分通道挂 @@ -110,21 +108,21 @@ ## 注释与编码 -- 关键字段、关键分支、关键约束、非直观实现补**简洁中文**注释。 -- 不要为省事删除原有有效注释;也不要写无信息量的注释。 -- 中文写入必须 UTF-8,并自查显示是否正常;不要用"改成英文"规避乱码。 +- 关键字段/分支/约束/非直观实现补**简洁中文**注释;中文写入必须 UTF-8,不要用"改成英文"规避乱码。 - superpowers 产出的功能文档(设计/实施/联调)默认中文落地;代码标识、文件路径、接口路径、SQL、命令保持原样不意译。 +## 文档输出格式 + +- 新写文档默认输出 **HTML 格式**(便于浏览器直接打开、自带样式)。 +- 例外:`docs/superpowers/` 下保持 markdown(工作流约定)。 +- 历史已有的 markdown 文档不强制迁移;只有新写的按 HTML。 + ## 工作规则(执行前对照) 1. 优先做有边界的模块内改动,避免跨模块扩散。 -2. 业务逻辑落 `*-boot`;可复用契约落 `*-api`;可复用框架能力落 `rdms-framework`。 -3. **不要修改** `application-local.yaml` / `application-dev.yaml`,除非任务本身就是环境配置调整。 -4. 把本地 YAML 当作可能带机器差异的文件,改前先查 git 状态。 -5. 包结构:`controller` / `service` / `dal` / `convert`,保持不变。 -6. 新增共享能力优先扩展现有 `rdms-spring-boot-starter-*`,不要在业务服务里重复堆配置。 -7. 改跨模块 API 时,提供方实现与对应 `*-api` 契约同步更新。 -8. **未经用户明确同意,不执行任何 `mvn`、启动命令、脚本等会实际运行项目的命令。** +2. **不要修改** `application-local.yaml` / `application-dev.yaml`,除非任务本身就是环境配置调整;改前先查 git 状态。 +3. 新增共享能力优先扩展现有 `rdms-spring-boot-starter-*`,不要在业务服务里重复堆配置。 +4. **未经用户明确同意,不执行任何 `mvn`、启动命令、脚本等会实际运行项目的命令。** ## Git 操作纪律 @@ -148,8 +146,6 @@ 3. **先挂救生圈**:建议用 `git tag backup-xxx ` 锁定当前 SHA,**再执行**破坏性命令。 4. **明示翻车回滚路径**:例如"如果不对,`git reset --hard backup-xxx` 即可回到此处"。 -与 memory `feedback_no_git_commands.md`、`feedback_main_branch_workflow.md` 衔接:用户已要求"不主动跑 git 子命令(含只读)"+"在 main 上工作";本节进一步约束——**即使是让用户执行的建议命令**,也必须先满足上述核实清单。 - ## 验证默认动作 先定义验证方式,再做修改。默认静态验证: @@ -163,6 +159,6 @@ ## 给后续我自己的提醒 - 仓库可能有未提交的本地改动,不要顺手覆盖与当前任务无关的编辑。 -- `docs/` 是当前工作上下文的一部分,不是归档;架构级修改前先查阅。 +- `docs/` 是当前工作上下文的一部分,不是归档;架构级修改前先查阅 [`docs/README.md`](./docs/README.md)。 - 根 `pom.xml` 统一版本与依赖;版本调整改根 pom,不要散落到子模块。 - 推荐使用 `Glob` / `Grep` / `Read` 等专用工具,避免用 Bash 做文件搜索/读取/编辑。 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 index 2b855c8..a8cae0b 100644 --- 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 @@ -39,6 +39,12 @@ public interface ErrorCodeConstants { ErrorCode PRODUCT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_001_026, "初始团队中存在非法角色"); ErrorCode PRODUCT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_001_027, "原产品经理在该产品已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试"); ErrorCode PRODUCT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_001_028, "内置产品角色【{}】未在 system_role 找到,请联系管理员"); + ErrorCode PRODUCT_MEMBER_USER_INVALID = new ErrorCode(1_008_001_029, "产品成员不是有效系统用户"); + // 批量新增(POST /project/product/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截 + ErrorCode PRODUCT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_001_030, "请勿在批量列表中重复添加同一成员"); + ErrorCode PRODUCT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_001_031, "批量新增不允许指定为经理,请通过编辑成员调整"); + // 批量移出(POST /project/product/{id}/members/batch/inactive)专用:同一请求内 memberId 重复 + ErrorCode PRODUCT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE = new ErrorCode(1_008_001_032, "请勿在批量移出列表中重复指定同一成员"); // ========== 产品需求 1-008-002-000 ========== ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在"); @@ -103,6 +109,11 @@ public interface ErrorCodeConstants { ErrorCode PROJECT_DIRECTION_NOT_MATCH_PRODUCT = new ErrorCode(1_008_002_032, "项目方向与所属产品方向不一致"); ErrorCode PROJECT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_002_033, "原项目经理在该项目已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试"); ErrorCode PROJECT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_002_034, "内置项目角色【{}】未在 system_role 找到,请联系管理员"); + // 批量新增(POST /project/project/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截 + ErrorCode PROJECT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_002_035, "请勿在批量列表中重复添加同一成员"); + ErrorCode PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_002_036, "批量新增不允许指定为经理,请通过编辑成员调整"); + // 批量移出(POST /project/project/{id}/members/batch/inactive)专用:同一请求内 memberId 重复 + ErrorCode PROJECT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE = new ErrorCode(1_008_002_037, "请勿在批量移出列表中重复指定同一成员"); // ========== 执行管理 1-008-003-000 ========== ErrorCode PROJECT_EXECUTION_NOT_EXISTS = new ErrorCode(1_008_003_000, "执行不存在"); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductMemberController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductMemberController.java index ff34dbf..d46795d 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductMemberController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductMemberController.java @@ -1,6 +1,8 @@ package com.njcn.rdms.module.project.controller.admin.product; import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO; @@ -42,6 +44,14 @@ public class ProductMemberController { return success(productMemberService.createProductMember(productId, reqVO)); } + @PostMapping("/{id}/members/batch") + @Operation(summary = "批量新增产品团队成员") + @Parameter(name = "id", description = "产品编号", required = true, example = "1024") + public CommonResult> batchCreateProductMembers(@PathVariable("id") Long productId, + @Valid @RequestBody ProductMemberBatchCreateReqVO reqVO) { + return success(productMemberService.batchCreateProductMembers(productId, reqVO)); + } + @PutMapping("/{id}/members/{memberId}") @Operation(summary = "调整产品团队成员角色") public CommonResult updateProductMember(@PathVariable("id") Long productId, @@ -60,4 +70,13 @@ public class ProductMemberController { return success(true); } + @PostMapping("/{id}/members/batch/inactive") + @Operation(summary = "批量移出产品团队成员") + @Parameter(name = "id", description = "产品编号", required = true, example = "1024") + public CommonResult batchInactiveProductMembers(@PathVariable("id") Long productId, + @Valid @RequestBody ProductMemberBatchInactiveReqVO reqVO) { + productMemberService.batchInactiveProductMembers(productId, reqVO); + return success(true); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/member/ProductMemberBatchCreateReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/member/ProductMemberBatchCreateReqVO.java new file mode 100644 index 0000000..8ca10f9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/member/ProductMemberBatchCreateReqVO.java @@ -0,0 +1,44 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.member; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 产品团队成员批量新增 Request VO") +@Data +public class ProductMemberBatchCreateReqVO { + + /** + * 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。 + * 经理角色(product_manager)由 Service 兜底拦截,不在此体现。 + */ + @Schema(description = "待新增的成员列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "请至少选择一名成员") + @Size(max = 200, message = "单次批量加入成员不能超过 200 人") + @Valid + private List members; + + @Schema(description = "批量新增成员项") + @Data + public static class Item { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002001") + @NotNull(message = "角色编号不能为空") + private Long roleId; + + @Schema(description = "备注", example = "本次批量加入") + @Size(max = 500, message = "备注长度不能超过500个字符") + private String remark; + + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/member/ProductMemberBatchInactiveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/member/ProductMemberBatchInactiveReqVO.java new file mode 100644 index 0000000..b00ee82 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/member/ProductMemberBatchInactiveReqVO.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.project.controller.admin.product.vo.member; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 产品团队成员批量移出 Request VO") +@Data +public class ProductMemberBatchInactiveReqVO { + + /** + * 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。 + * 经理、已失效、重复 memberId 等业务规则由 Service 兜底,不在此体现。 + */ + @Schema(description = "待移出的成员关系 ID 列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "请至少选择一名成员") + @Size(max = 200, message = "单次批量移出成员不能超过 200 人") + private List memberIds; + + @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/admin/project/ProjectMemberController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectMemberController.java index 520b3e6..6684bee 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectMemberController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectMemberController.java @@ -1,6 +1,8 @@ package com.njcn.rdms.module.project.controller.admin.project; import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO; @@ -42,6 +44,14 @@ public class ProjectMemberController { return success(projectMemberService.createProjectMember(projectId, reqVO)); } + @PostMapping("/{id}/members/batch") + @Operation(summary = "批量新增项目成员") + @Parameter(name = "id", description = "项目编号", required = true, example = "1024") + public CommonResult> batchCreateProjectMembers(@PathVariable("id") Long projectId, + @Valid @RequestBody ProjectMemberBatchCreateReqVO reqVO) { + return success(projectMemberService.batchCreateProjectMembers(projectId, reqVO)); + } + @PutMapping("/{id}/members/{memberId}") @Operation(summary = "调整项目成员角色") public CommonResult updateProjectMember(@PathVariable("id") Long projectId, @@ -60,4 +70,13 @@ public class ProjectMemberController { return success(true); } + @PostMapping("/{id}/members/batch/inactive") + @Operation(summary = "批量移出项目成员") + @Parameter(name = "id", description = "项目编号", required = true, example = "1024") + public CommonResult batchInactiveProjectMembers(@PathVariable("id") Long projectId, + @Valid @RequestBody ProjectMemberBatchInactiveReqVO reqVO) { + projectMemberService.batchInactiveProjectMembers(projectId, reqVO); + return success(true); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/member/ProjectMemberBatchCreateReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/member/ProjectMemberBatchCreateReqVO.java new file mode 100644 index 0000000..bab6486 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/member/ProjectMemberBatchCreateReqVO.java @@ -0,0 +1,44 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.member; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 项目成员批量新增 Request VO") +@Data +public class ProjectMemberBatchCreateReqVO { + + /** + * 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。 + * 经理角色(project_manager)由 Service 兜底拦截,不在此体现。 + */ + @Schema(description = "待新增的成员列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "请至少选择一名成员") + @Size(max = 200, message = "单次批量加入成员不能超过 200 人") + @Valid + private List members; + + @Schema(description = "批量新增成员项") + @Data + public static class Item { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002001") + @NotNull(message = "角色编号不能为空") + private Long roleId; + + @Schema(description = "备注", example = "本次批量加入") + @Size(max = 500, message = "备注长度不能超过500个字符") + private String remark; + + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/member/ProjectMemberBatchInactiveReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/member/ProjectMemberBatchInactiveReqVO.java new file mode 100644 index 0000000..898a8ca --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/member/ProjectMemberBatchInactiveReqVO.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.project.controller.admin.project.vo.member; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 项目成员批量移出 Request VO") +@Data +public class ProjectMemberBatchInactiveReqVO { + + /** + * 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。 + * 经理、已失效、重复 memberId、仍担任未关闭执行负责人等业务规则由 Service 兜底,不在此体现。 + */ + @Schema(description = "待移出的成员关系 ID 列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "请至少选择一名成员") + @Size(max = 200, message = "单次批量移出成员不能超过 200 人") + private List memberIds; + + @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/service/product/ProductMemberService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductMemberService.java index 37f1f43..acfb907 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductMemberService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductMemberService.java @@ -1,5 +1,7 @@ package com.njcn.rdms.module.project.service.product; +import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO; @@ -29,6 +31,15 @@ public interface ProductMemberService { */ Long createProductMember(Long productId, ProductMemberSaveReqVO reqVO); + /** + * 批量新增产品团队成员(不承担经理交接语义;事务性入库,全部成功或整体回滚)。 + * + * @param productId 产品编号 + * @param reqVO 请求参数 + * @return 新建的成员关系 ID 列表,顺序与入参 members 一致 + */ + List batchCreateProductMembers(Long productId, ProductMemberBatchCreateReqVO reqVO); + /** * 调整产品团队成员角色 * @@ -47,4 +58,15 @@ public interface ProductMemberService { */ void inactiveProductMember(Long productId, Long memberId, ProductMemberInactiveReqVO reqVO); + /** + * 批量移出产品团队成员(事务性更新,全部成功或整体回滚)。 + *

+ * 业务校验:同请求 memberId 重复、不存在/不属于当前产品、已失效、产品经理均拒绝; + * 经理判定与单条 inactive 一致,按 product.managerUserId 比对。 + * + * @param productId 产品编号 + * @param reqVO 请求参数 + */ + void batchInactiveProductMembers(Long productId, ProductMemberBatchInactiveReqVO reqVO); + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductMemberServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductMemberServiceImpl.java index e4d6598..c4b9b42 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductMemberServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductMemberServiceImpl.java @@ -5,6 +5,8 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.module.project.constant.ObjectActivityConstants; import com.njcn.rdms.module.project.constant.ObjectRoleConstants; import com.njcn.rdms.module.project.constant.ProductObjectConstants; +import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO; @@ -32,6 +34,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -66,6 +69,14 @@ public class ProductMemberServiceImpl implements ProductMemberService { ProductDO product = validateProductExists(productId); List members = userObjectRoleMapper.selectListByObject(ProductObjectConstants.OBJECT_TYPE, productId); Map roleMap = getRoleMap(members.stream().map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet())); + // 过滤掉 visible=0 的角色行(业务自动赋予的角色,如创建者 / 隐式观察者;不在团队列表显示); + // visible=null 视同显示,兼容旧数据未回填的场景。ACTIVE / INACTIVE 行一并过滤。 + members = members.stream() + .filter(m -> { + ObjectRoleRespDTO role = roleMap.get(m.getRoleId()); + return role == null || !Integer.valueOf(0).equals(role.getVisible()); + }) + .toList(); Map userMap = getUserMap(members.stream().map(UserObjectRoleDO::getUserId).collect(Collectors.toSet())); // 拆分 ACTIVE / INACTIVE: @@ -192,6 +203,85 @@ public class ProductMemberServiceImpl implements ProductMemberService { return member.getId(); } + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", + permission = ProductObjectConstants.PERMISSION_UPDATE) + public List batchCreateProductMembers(Long productId, ProductMemberBatchCreateReqVO reqVO) { + validateProductEditable(productId); + + List items = reqVO.getMembers(); + // 同请求内 userId 重复一律拒绝(user-level),即使是不同角色也算重复——批量语义就是一人一行 + Set seenUserIds = new HashSet<>(items.size()); + for (ProductMemberBatchCreateReqVO.Item item : items) { + if (!seenUserIds.add(item.getUserId())) { + throw exception(ErrorCodeConstants.PRODUCT_MEMBER_BATCH_USER_DUPLICATE); + } + } + // 批量校验用户存在/启用——产品域单条接口未做此校验(历史负债),批量接口按需求 §4 兜底 + validateMemberUsers(seenUserIds); + + List memberIds = new ArrayList<>(items.size()); + LocalDateTime now = LocalDateTime.now(); + for (ProductMemberBatchCreateReqVO.Item item : items) { + ObjectRoleRespDTO targetRole = validateProductRole(item.getRoleId()); + if (isManagerRole(targetRole)) { + throw exception(ErrorCodeConstants.PRODUCT_MEMBER_BATCH_MANAGER_NOT_ALLOWED); + } + // user-level 拒绝:该用户在本产品下有任意 ACTIVE 行(含其他角色),不允许再批量加入 + List activeRows = userObjectRoleMapper + .selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, productId, item.getUserId()); + if (!activeRows.isEmpty()) { + throw exception(ErrorCodeConstants.PRODUCT_MEMBER_ALREADY_EXISTS); + } + + // 多角色支持:按 (user, object, role) 三元组判存在;INACTIVE 行复活,避免唯一索引 INSERT 冲突 + UserObjectRoleDO existingMember = userObjectRoleMapper + .selectByObjectUserAndRole(ProductObjectConstants.OBJECT_TYPE, productId, + item.getUserId(), targetRole.getId()); + UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember); + UserObjectRoleDO member; + String actionType; + if (existingMember == null) { + member = new UserObjectRoleDO(); + member.setUserId(item.getUserId()); + member.setObjectType(ProductObjectConstants.OBJECT_TYPE); + member.setObjectId(productId); + member.setRoleId(targetRole.getId()); + member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE); + member.setJoinedTime(now); + member.setLeftTime(null); + member.setRemark(normalizeNullableText(item.getRemark())); + userObjectRoleMapper.insert(member); + actionType = ObjectActivityConstants.MEMBER_ACTION_ADD; + } else { + member = existingMember; + member.setRoleId(targetRole.getId()); + member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE); + member.setJoinedTime(now); + member.setLeftTime(null); + member.setRemark(normalizeNullableText(item.getRemark())); + userObjectRoleMapper.updateById(member); + actionType = ObjectActivityConstants.MEMBER_ACTION_REACTIVATE; + } + writeMemberAuditLog(member, actionType, before, member, null); + memberIds.add(member.getId()); + } + return memberIds; + } + + private void validateMemberUsers(Set userIds) { + try { + Boolean valid = adminUserApi.validateUserList(new ArrayList<>(userIds)).getCheckedData(); + if (Boolean.TRUE.equals(valid)) { + return; + } + } catch (RuntimeException ex) { + throw exception(ErrorCodeConstants.PRODUCT_MEMBER_USER_INVALID); + } + throw exception(ErrorCodeConstants.PRODUCT_MEMBER_USER_INVALID); + } + @Override @Transactional(rollbackFor = Exception.class) @CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", @@ -257,6 +347,80 @@ public class ProductMemberServiceImpl implements ProductMemberService { normalizeNullableText(reqVO.getReason())); } + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId", + permission = ProductObjectConstants.PERMISSION_UPDATE) + public void batchInactiveProductMembers(Long productId, ProductMemberBatchInactiveReqVO reqVO) { + ProductDO product = validateProductEditable(productId); + + List memberIds = reqVO.getMemberIds(); + // 同请求 memberId 重复一律拒绝,避免对同一行重复 update 造成 audit 重复落库 + Set seen = new HashSet<>(memberIds.size()); + for (Long memberId : memberIds) { + if (!seen.add(memberId)) { + throw exception(ErrorCodeConstants.PRODUCT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE); + } + } + + String reason = normalizeNullableText(reqVO.getReason()); + LocalDateTime now = LocalDateTime.now(); + // 收集批次摘要,循环外聚合写一条审计日志(需求 §6 口径) + List removed = new ArrayList<>(memberIds.size()); + for (Long memberId : memberIds) { + UserObjectRoleDO member = validateMemberExists(productId, memberId); + if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) { + throw exception(ErrorCodeConstants.PRODUCT_MEMBER_NOT_ACTIVE); + } + // 经理判定沿用单条 inactive 口径:按 product.managerUserId 比对,不引入 role.code 判定避免分叉 + if (Objects.equals(member.getUserId(), product.getManagerUserId())) { + throw exception(ErrorCodeConstants.PRODUCT_MANAGER_MEMBER_NOT_ALLOW_REMOVE); + } + + member.setStatus(ObjectRoleConstants.MEMBER_STATUS_INACTIVE); + member.setLeftTime(now); + userObjectRoleMapper.updateById(member); + removed.add(member); + } + + writeBatchInactiveAuditLog(productId, removed, reason); + } + + /** + * 批量移出场景的聚合审计:写一条对象维度日志(bizType=product, bizId=productId), + * fieldChanges 体现批量摘要 batchCount + members 数组,与 writeManagerChangeAuditLog 风格对齐。 + * 单条 before/after 字段差异在聚合视图下故意丢弃,由读侧按需展开。 + */ + private void writeBatchInactiveAuditLog(Long productId, List members, String reason) { + if (members.isEmpty()) { + return; + } + BizAuditLogDO auditLog = new BizAuditLogDO(); + auditLog.setBizType(ProductObjectConstants.OBJECT_TYPE); + auditLog.setBizId(productId); + auditLog.setActionType(ObjectActivityConstants.MEMBER_ACTION_REMOVE); + auditLog.setFieldChanges(buildBatchInactiveFieldChanges(members)); + auditLog.setReason(reason); + auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + bizAuditLogMapper.insert(auditLog); + } + + private String buildBatchInactiveFieldChanges(List members) { + List> memberSummaries = new ArrayList<>(members.size()); + for (UserObjectRoleDO member : members) { + Map summary = new LinkedHashMap<>(); + summary.put("memberId", member.getId()); + summary.put("userId", member.getUserId()); + summary.put("roleId", member.getRoleId()); + memberSummaries.add(summary); + } + Map root = new LinkedHashMap<>(); + root.put("batchCount", members.size()); + root.put("members", memberSummaries); + return JsonUtils.toJsonString(root); + } + private ProductDO validateProductExists(Long productId) { if (productId == null) { throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectMemberService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectMemberService.java index e5184cc..e3ccb21 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectMemberService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectMemberService.java @@ -1,5 +1,7 @@ package com.njcn.rdms.module.project.service.project; +import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO; @@ -16,8 +18,28 @@ public interface ProjectMemberService { Long createProjectMember(Long projectId, ProjectMemberSaveReqVO reqVO); + /** + * 批量新增项目成员(不承担经理交接语义;事务性入库,全部成功或整体回滚)。 + * + * @param projectId 项目编号 + * @param reqVO 请求参数 + * @return 新建的成员关系 ID 列表,顺序与入参 members 一致 + */ + List batchCreateProjectMembers(Long projectId, ProjectMemberBatchCreateReqVO reqVO); + void updateProjectMember(Long projectId, Long memberId, ProjectMemberUpdateReqVO reqVO); void inactiveProjectMember(Long projectId, Long memberId, ProjectMemberInactiveReqVO reqVO); + /** + * 批量移出项目成员(事务性更新,全部成功或整体回滚)。 + *

+ * 业务校验:同请求 memberId 重复、不存在/不属于当前项目、已失效、项目经理、仍担任未关闭执行负责人均拒绝; + * 经理判定与单条 inactive 一致,按 project.managerUserId 比对。 + * + * @param projectId 项目编号 + * @param reqVO 请求参数 + */ + void batchInactiveProjectMembers(Long projectId, ProjectMemberBatchInactiveReqVO reqVO); + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectMemberServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectMemberServiceImpl.java index 818c6b8..ee11cd9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectMemberServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectMemberServiceImpl.java @@ -6,6 +6,8 @@ import com.njcn.rdms.module.project.constant.ObjectActivityConstants; import com.njcn.rdms.module.project.constant.ObjectRoleConstants; import com.njcn.rdms.module.project.constant.ProjectExecutionConstants; import com.njcn.rdms.module.project.constant.ProjectObjectConstants; +import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO; @@ -34,6 +36,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -70,6 +73,14 @@ public class ProjectMemberServiceImpl implements ProjectMemberService { ProjectDO project = validateProjectExists(projectId); List members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId); Map roleMap = getRoleMap(members.stream().map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet())); + // 过滤掉 visible=0 的角色行(业务自动赋予的角色,如创建者 / 隐式观察者;不在团队列表显示); + // visible=null 视同显示,兼容旧数据未回填的场景。ACTIVE / INACTIVE 行一并过滤。 + members = members.stream() + .filter(m -> { + ObjectRoleRespDTO role = roleMap.get(m.getRoleId()); + return role == null || !Integer.valueOf(0).equals(role.getVisible()); + }) + .toList(); Map userMap = getUserMap(members.stream().map(UserObjectRoleDO::getUserId).collect(Collectors.toSet())); // 拆分 ACTIVE / INACTIVE: @@ -188,6 +199,78 @@ public class ProjectMemberServiceImpl implements ProjectMemberService { return member.getId(); } + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectObjectConstants.PERMISSION_MEMBER) + public List batchCreateProjectMembers(Long projectId, ProjectMemberBatchCreateReqVO reqVO) { + validateProjectEditable(projectId); + + List items = reqVO.getMembers(); + // 同请求内 userId 重复一律拒绝(user-level),即使是不同角色也算重复——批量语义就是一人一行 + Set seenUserIds = new HashSet<>(items.size()); + for (ProjectMemberBatchCreateReqVO.Item item : items) { + if (!seenUserIds.add(item.getUserId())) { + throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_USER_DUPLICATE); + } + } + // 批量校验用户存在/启用——按需求 §4 兜底 + validateMemberUsers(seenUserIds); + + List memberIds = new ArrayList<>(items.size()); + LocalDateTime now = LocalDateTime.now(); + for (ProjectMemberBatchCreateReqVO.Item item : items) { + ObjectRoleRespDTO targetRole = validateProjectRole(item.getRoleId()); + if (isManagerRole(targetRole)) { + throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED); + } + // user-level 拒绝:该用户在本项目下有任意 ACTIVE 行(含其他角色),不允许再批量加入 + List activeRows = userObjectRoleMapper + .selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, projectId, item.getUserId()); + if (!activeRows.isEmpty()) { + throw exception(ErrorCodeConstants.PROJECT_MEMBER_ALREADY_EXISTS); + } + + // 多角色支持:按 (user, object, role) 三元组判存在;INACTIVE 行复活,避免唯一索引 INSERT 冲突 + UserObjectRoleDO existingMember = userObjectRoleMapper + .selectByObjectUserAndRole(ProjectObjectConstants.OBJECT_TYPE, projectId, + item.getUserId(), targetRole.getId()); + UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember); + UserObjectRoleDO member = existingMember == null ? new UserObjectRoleDO() : existingMember; + member.setUserId(item.getUserId()); + member.setObjectType(ProjectObjectConstants.OBJECT_TYPE); + member.setObjectId(projectId); + member.setRoleId(targetRole.getId()); + member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE); + member.setJoinedTime(now); + member.setLeftTime(null); + member.setRemark(normalizeNullableText(item.getRemark())); + String actionType; + if (existingMember == null) { + userObjectRoleMapper.insert(member); + actionType = ObjectActivityConstants.MEMBER_ACTION_ADD; + } else { + userObjectRoleMapper.updateById(member); + actionType = ObjectActivityConstants.MEMBER_ACTION_REACTIVATE; + } + writeMemberAuditLog(member, actionType, before, member, null); + memberIds.add(member.getId()); + } + return memberIds; + } + + private void validateMemberUsers(Set userIds) { + try { + Boolean valid = adminUserApi.validateUserList(new ArrayList<>(userIds)).getCheckedData(); + if (Boolean.TRUE.equals(valid)) { + return; + } + } catch (RuntimeException ex) { + throw exception(ErrorCodeConstants.PROJECT_MEMBER_USER_INVALID); + } + throw exception(ErrorCodeConstants.PROJECT_MEMBER_USER_INVALID); + } + @Override @Transactional(rollbackFor = Exception.class) @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", @@ -253,6 +336,47 @@ public class ProjectMemberServiceImpl implements ProjectMemberService { normalizeNullableText(reqVO.getReason())); } + @Override + @Transactional(rollbackFor = Exception.class) + @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", + permission = ProjectObjectConstants.PERMISSION_MEMBER) + public void batchInactiveProjectMembers(Long projectId, ProjectMemberBatchInactiveReqVO reqVO) { + ProjectDO project = validateProjectEditable(projectId); + + List memberIds = reqVO.getMemberIds(); + // 同请求 memberId 重复一律拒绝,避免对同一行重复 update 造成 audit 重复落库 + Set seen = new HashSet<>(memberIds.size()); + for (Long memberId : memberIds) { + if (!seen.add(memberId)) { + throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE); + } + } + + String reason = normalizeNullableText(reqVO.getReason()); + LocalDateTime now = LocalDateTime.now(); + // 收集批次摘要,循环外聚合写一条审计日志(需求 §6 口径) + List removed = new ArrayList<>(memberIds.size()); + for (Long memberId : memberIds) { + UserObjectRoleDO member = validateMemberExists(projectId, memberId); + if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) { + throw exception(ErrorCodeConstants.PROJECT_MEMBER_NOT_ACTIVE); + } + // 经理判定沿用单条 inactive 口径:按 project.managerUserId 比对,不引入 role.code 判定避免分叉 + if (Objects.equals(member.getUserId(), project.getManagerUserId())) { + throw exception(ErrorCodeConstants.PROJECT_MANAGER_MEMBER_NOT_ALLOW_REMOVE); + } + // 与单条 inactive 一致:仍担任未关闭执行负责人的成员不能直接移出,需先完成执行负责人交接 + validateNoOpenOwnedExecutions(projectId, member.getUserId()); + + member.setStatus(ObjectRoleConstants.MEMBER_STATUS_INACTIVE); + member.setLeftTime(now); + userObjectRoleMapper.updateById(member); + removed.add(member); + } + + writeBatchInactiveAuditLog(projectId, removed, reason); + } + private ProjectDO validateProjectExists(Long projectId) { if (projectId == null) { throw exception(ErrorCodeConstants.PROJECT_NOT_EXISTS); @@ -485,6 +609,41 @@ public class ProjectMemberServiceImpl implements ProjectMemberService { bizAuditLogMapper.insert(auditLog); } + /** + * 批量移出场景的聚合审计:写一条对象维度日志(bizType=project, bizId=projectId), + * fieldChanges 体现批量摘要 batchCount + members 数组,与 writeManagerChangeAuditLog 风格对齐。 + * 单条 before/after 字段差异在聚合视图下故意丢弃,由读侧按需展开。 + */ + private void writeBatchInactiveAuditLog(Long projectId, List members, String reason) { + if (members.isEmpty()) { + return; + } + BizAuditLogDO auditLog = new BizAuditLogDO(); + auditLog.setBizType(ProjectObjectConstants.OBJECT_TYPE); + auditLog.setBizId(projectId); + auditLog.setActionType(ObjectActivityConstants.MEMBER_ACTION_REMOVE); + auditLog.setFieldChanges(buildBatchInactiveFieldChanges(members)); + auditLog.setReason(reason); + auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + bizAuditLogMapper.insert(auditLog); + } + + private String buildBatchInactiveFieldChanges(List members) { + List> memberSummaries = new ArrayList<>(members.size()); + for (UserObjectRoleDO member : members) { + Map summary = new LinkedHashMap<>(); + summary.put("memberId", member.getId()); + summary.put("userId", member.getUserId()); + summary.put("roleId", member.getRoleId()); + memberSummaries.add(summary); + } + Map root = new LinkedHashMap<>(); + root.put("batchCount", members.size()); + root.put("members", memberSummaries); + return JsonUtils.toJsonString(root); + } + private String buildMemberFieldChanges(UserObjectRoleDO before, UserObjectRoleDO after) { Map fieldChanges = new LinkedHashMap<>(); appendFieldChange(fieldChanges, "userId", valueOf(before, UserObjectRoleDO::getUserId), diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/permission/dto/ObjectRoleRespDTO.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/permission/dto/ObjectRoleRespDTO.java index 5f50d0c..13148f9 100644 --- a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/permission/dto/ObjectRoleRespDTO.java +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/permission/dto/ObjectRoleRespDTO.java @@ -22,4 +22,8 @@ public class ObjectRoleRespDTO { @Schema(description = "对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "product") private String objectType; + @Schema(description = "是否显示在角色下拉中:1 显示 / 0 不显示;业务自动赋予角色为 0", + requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer visible; + } diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RoleRespVO.java index 20f84d0..23edf0a 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RoleRespVO.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/permission/vo/role/RoleRespVO.java @@ -41,6 +41,10 @@ public class RoleRespVO { @Schema(description = "角色类型,参见 RoleTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer type; + @Schema(description = "是否显示在角色下拉中:1 显示 / 0 不显示;业务自动赋予角色为 0,前端下拉应过滤掉", + requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer visible; + @Schema(description = "备注", example = "我是一个角色") private String remark; 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 0f83d3a..0f65b10 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 @@ -62,6 +62,12 @@ public class RoleDO extends BaseDO { * 枚举 {@link RoleTypeEnum} */ private Integer type; + /** + * 是否显示在角色下拉中:1 显示 / 0 不显示 + * + * 业务流程自动赋予的角色(如创建者、隐式观察者)设为 0,前端下拉过滤掉,避免用户手动选择 + */ + private Integer visible; /** * 备注 */