53 Commits

Author SHA1 Message Date
dk
55a50eb3d5 fix(加班申请): 修复加班申请的状态机相关代码的不合理的地方。 2026-06-03 21:00:11 +08:00
caozehui
679edf08ba fix(person-item): 权限放开 2026-06-02 09:14:40 +08:00
dk
e71140d8a2 feat(新增加班申请功能): 新增申请功能,可在工作台进行审核。
fix(dict_data): 字典数据的颜色类型字段不允许null更新。
2026-06-01 21:25:02 +08:00
5c7dbf7286 docs(guide): 更新对象域权限文档与数据库连接配置
- 修改开发环境和本地环境的数据库连接从 rdms_v3 切换到 rdms_view
- 更新 CLAUDE.md 中的对象域权限校验说明,统一在 Service 层使用 @CheckObjectPermission
- 移除 ExecutionAssigneeMapper 中的废弃查询方法和相关注解
- 优化 ObjectPermissionService 中的权限码描述信息
- 新增执行查询权限常量 PERMISSION_QUERY
- 重构 ProjectExecutionMapper 分页查询逻辑,使用 @Select 注解替代 LambdaQueryWrapper
- 添加执行状态面板和截止时间范围过滤功能
- 在 ProjectExecutionServiceImpl 中集成对象域权限检查
- 更新状态面板服务中的权限校验注解配置
2026-05-29 16:24:09 +08:00
9f03dc27cc fix(security): 修复token认证过滤器异步刷新异常处理
- 添加Slf4j注解用于日志记录
- 在load方法中添加try-catch块捕获ServiceException异常
- 当远端token过期或校验失败时返回LOGIN_USER_EMPTY而不是抛出异常
- 记录token校验失败的日志信息避免被Guava包装为ExecutionException
- 防止异步刷新线程将预期的验证异常作为未捕获异常打印到日志中
2026-05-26 19:11:17 +08:00
d669d53a80 feat(project): 添加项目进度自动计算功能
- 在 ProjectMapper 中新增 updateProgressRateById 方法,支持单独更新项目进度
- 在 ProjectService 中新增 recalcProgress 接口,用于重新计算项目进度
- 实现 ProjectServiceImpl 的进度重计算逻辑,通过根任务平均进度更新项目进度
- 新增 ProjectTaskMapper 的 selectRootTaskAvgProgressByProjectId 查询方法
- 在任务创建、更新、删除、状态变更等操作后触发项目进度重计算
- 添加进度归一化处理,确保数值精度为两位小数
- 更新 CLAUDE.md 文档,加强技术风险判断要求
2026-05-25 14:17:37 +08:00
df13a90107 feat(project): 添加项目任务跨执行聚合查询功能
- 新增 ObjectPermissionService.hasPermission 非抛模式权限检查方法
- 实现 ProjectObjectPermissionService.hasPermission 权限验证逻辑
- 为 ProductObjectPermissionService 预留空实现并添加日志警告
- 在 ProjectExecutionController 中支持负数 pageSize 查询全部功能
- 添加 ProjectTaskConstants.PERMISSION_LIST_ALL 权限码常量定义
- 扩展 ProjectTaskMapper 支持跨执行聚合分页、状态统计和摘要查询
- 更新 ProjectTaskRespVO 包含执行名称和状态码字段
- 实现 ProjectTaskService.assembleTaskRespVOPageCrossExecution 跨执行装配方法
- 优化任务服务中的执行信息批量回填和生命周期应用逻辑
- 统一使用服务器时区 Asia/Shanghai 处理日期时间操作
- 为 .claude 设置添加新的代码搜索和分析命令
2026-05-23 14:18:15 +08:00
8a36b49128 feat(project): 添加项目任务跨执行聚合查询功能
- 新增 ObjectPermissionService.hasPermission 非抛模式权限检查方法
- 实现 ProjectObjectPermissionService.hasPermission 权限验证逻辑
- 为 ProductObjectPermissionService 预留空实现并添加日志警告
- 在 ProjectExecutionController 中支持负数 pageSize 查询全部功能
- 添加 ProjectTaskConstants.PERMISSION_LIST_ALL 权限码常量定义
- 扩展 ProjectTaskMapper 支持跨执行聚合分页、状态统计和摘要查询
- 更新 ProjectTaskRespVO 包含执行名称和状态码字段
- 实现 ProjectTaskService.assembleTaskRespVOPageCrossExecution 跨执行装配方法
- 优化任务服务中的执行信息批量回填和生命周期应用逻辑
- 统一使用服务器时区 Asia/Shanghai 处理日期时间操作
- 为 .claude 设置添加新的代码搜索和分析命令
2026-05-23 14:18:04 +08:00
dk
c9549bed46 feat(新增需求评审功能): 新增需求评审功能。
fix(产品需求、项目需求): 按照会议意见修改诸多细节。
fix(产品对象域的概览界面): 提供相应接口给前端。
2026-05-22 13:52:29 +08:00
dk
5caf3bbdc9 Merge branch 'main' of http://192.168.1.22:3000/Microservice/cn-rdms 2026-05-22 13:51:17 +08:00
caozehui
9e4f8becc8 docs(ticket): update child split entry 2026-05-22 13:46:01 +08:00
dk
58eed8234a feat(新增需求评审功能): 新增需求评审功能。
fix(产品需求、项目需求): 按照会议意见修改诸多细节。
fix(产品对象域的概览界面): 提供相应接口给前端。
2026-05-22 13:45:24 +08:00
caozehui
b4e9685344 docs(ticket): add ticket design spec 2026-05-22 13:36:58 +08:00
caozehui
2ad9a4e206 fix(dict): 添加字典类型参量 2026-05-22 10:02:58 +08:00
fd637ae604 chore(git): 添加 Claude Code 本地工作区到忽略列表
- 在 .gitignore 中新增 .claude/ 目录
- 防止本地工作区文件被提交到仓库
- 保持仓库整洁避免临时文件污染
2026-05-21 21:19:55 +08:00
1bee5eb05b fix(project): 修复项目执行管理中的多项问题并优化字典数据功能
- 修复字典数据分页接口命名错误,从 getDictTypePage 改为 getDictDataPage
- 修复字典数据查询排序逻辑,从 orderByDesc 改为 orderByAsc 并增加 id 排序
- 更新字典数据分页请求参数验证,将 dictType 设为必填项并添加非空验证
- 在字典数据简单响应对象中添加备注字段
- 修复项目执行删除权限验证,允许非初始态执行删除但阻止已完成执行删除
- 添加项目执行和任务优先级验证错误码常量
- 优化项目执行删除逻辑,支持级联软删相关任务、工作日志和协办数据
- 添加项目需求关联验证,防止无效需求关联到执行
- 修复执行协办数据批量删除方法的参数验证逻辑
- 添加工时完成难度验证错误码,完善项目需求删除前检查机制
- 更新 CLAUDE.md 文档,补充种子 SQL 编写规范和雪花 ID 处理说明
2026-05-21 21:17:54 +08:00
caozehui
19637d74a4 fix(personal-item): 个人事项&任务添加type类型字段 2026-05-21 13:59:45 +08:00
caozehui
d069948d2a feat(personal-item): 个人事项 2026-05-21 10:31:09 +08:00
caozehui
3199c876c3 fix(file): 上传文件微调 2026-05-19 14:48:18 +08:00
caozehui
b6d31ab156 feat(personal-center): 个人头像更新 2026-05-19 10:56:13 +08:00
1ef86fc1cb feat(guidelines): 更新工作指引并添加批量成员管理功能
- 简化 AGENTS.md 内容,统一引用 CLAUDE.md 作为主要指引
- 更新 CLAUDE.md 中的工作方式和验证流程说明
- 添加产品和项目成员批量新增/移出的错误码定义
- 扩展系统角色 API 响应 DTO,增加可见性字段
- 实现产品团队成员批量新增和批量移出控制器接口
- 添加产品成员批量操作的服务层实现和业务校验逻辑
- 实现项目团队成员批量操作的相关控制器接口
- 优化产品成员列表查询,过滤不可见角色行
- 添加批量操作的审计日志记录功能
2026-05-18 21:16:11 +08:00
dk
75886d7af5 fix(产品需求、项目需求): 按照会议所说进行修改。 2026-05-18 16:44:29 +08:00
50b84a57bb fix(gateway): 优化网关连接池和服务端保活配置
- 配置网关HttpClient连接池max-idle-time为30s,确保小于下游服务keep-alive-timeout
- 设置网关连接超时时间为10s,响应超时时间为30s
- 配置下游服务tomcat keep-alive-timeout为60s,避免与网关连接池形成竞争条件
- 将灰度负载均衡器日志级别从warn调整为debug,减少本地开发环境日志噪音
- 添加系统性调试技能配置到Claude设置中
2026-05-18 08:28:33 +08:00
caozehui
bd05f6d593 feat(personal-center): 实现个人信息功能 2026-05-15 15:52:50 +08:00
470096aa9a feat(gateway): 修改文件上传接口返回结构并添加认证路径白名单
- 将 POST /system/file/upload 接口返回结构从字符串改为 { id: string, url: string } 对象
- 添加 id 字段作为 infra_file.id 的字符串形式,解决前端精度丢失问题
- 新增 SKIP_AUTH_PATHS 白名单集合,包含登录、登出、刷新令牌等免校验路径
- 在网关过滤器中添加白名单检查逻辑,跳过指定路径的 access token 校验
- 解决过期 token 拦截导致刷新令牌接口无法正常执行的问题
2026-05-15 13:38:06 +08:00
4ad2ddeabe feat(file): 扩展文件上传响应信息并增强匿名文件访问安全控制
- 在FileUploadRespVO中增加configId和path字段,丰富文件上传返回信息
- 新增selectByConfigIdAndPath方法用于按配置ID和路径查询文件记录
- 在getFileContent服务中添加存在性校验,防止已删除文件被匿名访问
- 更新getFileContent接口注释为"获取文件内容(匿名)",明确使用场景
- 为图片文件添加缓存控制头,设置max-age为一天并使用ETag实现条件缓存
- 通过DigestUtil计算文件内容SHA256作为ETag值,优化CDN和网关层缓存命中
2026-05-15 09:48:36 +08:00
caozehui
9ee49b1863 feat(project): 新增对象状态模型与状态流转管理能力
- 新增对象状态模型和状态流转的后台管理接口
  - 补充分页查询、增删改查及批量删除能力
  - 增加状态编码、初始状态和流转配置的唯一性校验
  - 增加状态引用校验和删除前校验
  - 统一 Swagger 注解依赖为 jakarta 版本以适配 Spring Boot 3
2026-05-15 09:21:10 +08:00
be7e0d6162 feat(project): 添加项目成员响应VO中的附加角色名称字段
- 引入 ArraySchema 注解用于描述角色名称数组
- 添加 additionalRoleNames 字段存储非主角色的中文名列表
- 设置默认值为 Collections.emptyList() 确保单角色时为空数组
- 提供多角色场景支持(如同人 manager + creator)
- 完善注释说明字段用途和使用场景
2026-05-14 13:58:55 +08:00
8f6b762bf3 feat(system): 扩展用户部门权限功能
- 在 AdminUserService 中新增 listEnabledUserIdsByDeptIds 方法获取指定部门集合下启用且未离职的用户 ID 集合
- 在 DeptService 中新增 listDescendantDeptIds 方法获得指定部门集合及其所有子孙部门的 ID 集合
- 在 DeptService 中新增 listCodesByIds 方法按 id 集合批量查询部门 code 集合
- 在 OrgLeaderRelationService 中新增 listEffectiveDeptIdsByUserId 方法查询指定用户当前生效的负责人关系所对应的 dept_id 集合
- 在 PermissionApi 中新增 isSuperAdmin 接口判断用户是否超管
- 在 ObjectPermissionApi 中新增 getObjectRolePermissionDetailMerged 接口按 roleId 列表聚合菜单 + 权限码
- 扩展 ProductContextRoleRespVO 添加多角色场景的附加角色名称列表
- 扩展 ProductCreateWithTeamReqVO 支持创建时添加关心人用户 ID 列表
- 优化 ProductMemberServiceImpl 支持同一用户多角色显示,区分主角色和附加角色
- 新增 MEMBER_ACTION_REACTIVATE 复活动作类型用于处理 INACTIVE 成员行重新激活场景
- 在 ObjectStatusModelDO 中新增 progressExcludedFlag 字段控制是否参与上层进度统计
- 更新 AGENTS.md 和 CLAUDE.md 添加 Git 操作纪律规范
- 在 rdms-project-api 中新增多个错误码常量支持角色转移和内置角色配置验证
2026-05-14 13:58:40 +08:00
dk
3946c0a0aa feat(项目需求): 开发项目需求的富文本和附件功能。 2026-05-13 22:55:31 +08:00
dk
e1db030c37 Merge branch 'main' of http://192.168.1.22:3000/Microservice/cn-rdms 2026-05-13 20:56:58 +08:00
dk
544b56a5d9 feat(项目需求): 开发项目需求。 2026-05-13 20:56:48 +08:00
dk
7b4edd6b59 feat(项目需求): 开发项目需求。 2026-05-13 20:56:16 +08:00
dk
43d8be724e feat(项目需求): 开发项目需求。 2026-05-13 20:54:17 +08:00
220dec9b6c feat(file): 改造文件上传接口返回结构
- 将 POST /system/file/upload 接口返回结构从 string 改为 { id: string, url: string }
- id 字段以字符串形式返回 infra_file.id,避免 JavaScript 数值精度丢失问题
- 保持接口路径、方法和入参完全不变,仅修改返回格式
- 添加 GET /system/file/download 接口用于文件下载功能
- 优化 AppFileController 中的文件上传实现逻辑
- 更新 AuthConvert 和 AuthUserInfoRespVO 添加用户昵称和头像字段
- 在 CLAUDE.md 中补充鉴权通道和 HTTP 动词语义说明文档
- 在 ErrorCodeConstants.java 中添加多个项目管理和执行相关的错误码定义
- 删除执行成员相关的数据库表和接口定义(执行协办人替代方案)
- 在 FileMapper 中增加按 URL 查询文件的方法支持
2026-05-12 21:32:32 +08:00
dk
4f6b209c3d fix(产品需求): 完善产品需求的诸多细节。 2026-05-09 18:01:42 +08:00
dk
604bf61981 fix(产品需求): 解决测试后存在的一些问题。 2026-05-09 13:44:38 +08:00
dk
7575784c01 Merge branch 'main' of http://192.168.1.22:3000/Microservice/cn-rdms 2026-05-09 13:36:47 +08:00
dk
7399f8c3da fix(产品需求): 定义并修改一些常量。 2026-05-09 13:36:31 +08:00
6f33ab9c05 feat(project): 补充项目、执行、任务相关能力 2026-05-09 10:37:27 +08:00
dk
36752d1d15 fix(产品需求): 修复产品需求使用状态和终止态字典的问题 2026-05-07 17:07:52 +08:00
dk
73360d70ce fix(产品需求): 修复产品需求查询的问题、修复产品需求树返回数据的问题 2026-05-07 11:10:21 +08:00
dk
7913c210cd feat(产品需求): 产品需求相关代码 2026-05-06 17:49:30 +08:00
dk
06d29210ba Merge branch 'main' of http://192.168.1.22:3000/Microservice/cn-rdms
# Conflicts:
#	rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductServiceImpl.java
2026-04-28 16:58:35 +08:00
dk
b4e1aae062 feat(产品需求): 产品和产品需求相关的测试类 2026-04-28 16:53:33 +08:00
dk
9ad7e063c0 feat(产品需求): 产品和产品需求相关的测试类 2026-04-28 16:50:04 +08:00
dk
846348e1aa feat(user): 支持前端用用户昵称字段进行模糊搜索
fix(post): 使岗位排序能按照sort字段来排序。
2026-04-28 16:43:38 +08:00
ae90dcec24 feat(project): 为项目活动时间线添加成员角色名称显示功能
- 在 ObjectActivityConstants 中添加 MEMBER_ACTION_UPDATE 类型支持
- 为 ProductActivityQueryService 和 ProductActivityTimelineQueryService
  添加角色名称加载和缓存功能
- 实现角色名称解析和 JSON 数据结构扩展
- 添加相关单元测试验证角色名称显示逻辑
- 集成 ObjectPermissionApi 获取角色信息并实现缓存机制
2026-04-24 16:22:23 +08:00
ee732b97bf feat(project): 新增产品动态时间线接口并重构活动查询逻辑
- 新增 GET /project/product/{id}/activities/page 接口用于产品动态时间线分页查询
- 添加 ProductActivityTimelinePageReqVO 和 ProductActivityTimelineRespVO 数据传输对象
- 实现 ProductActivityTimelineQueryService 服务处理动态时间线查询逻辑
- 在 BizAuditLogMapper 中新增按业务类型和动作类型查询的方法
- 在 ProductStatusLogMapper 中新增按产品ID和动作类型查询的方法
- 将硬编码的活动类型常量抽取到 ObjectActivityConstants 统一管理
- 重构 ProductActivityQueryService 使用统一的常量和查询方法
- 更新 ProductMemberServiceImpl 和 ProductServiceImpl 使用新的活动常量
- 添加相应的单元测试验证新接口和查询逻辑的正确性
- 新增产品对象首页改版设计文档和产品动态时间线接口需求说明文档
2026-04-24 15:43:38 +08:00
0a6d70f7cf feat(permission): 新增对象权限API接口及实现
- 定义ObjectPermissionApi接口提供对象作用域权限查询功能
- 实现ObjectPermissionApiImpl提供角色权限查询和转换逻辑
- 添加ObjectMenuRespDTO、ObjectRoleRespDTO和ObjectRolePermissionRespDTO数据传输对象
- 实现按角色ID、角色编码查询对象作用域角色及权限的功能
- 提供获取对象作用域角色菜单与权限聚合结果的方法
- 添加完整单元测试覆盖对象权限API的主要业务场景
2026-04-23 09:23:33 +08:00
156728b1b9 feat(permission): 重构权限系统实现对象级别权限控制
- 在PermissionService中新增getScopedMenusByRoleId和getScopedPermissionsByRoleId方法
- 实现getScopedMenusByRoleId方法用于获取角色的对象范围菜单列表
- 实现getScopedPermissionsByRoleId方法用于获取角色的对象范围权限集合
- 添加getEnabledScopedRole私有方法确保只处理启用状态的角色对象
- 在ProductMemberServiceImpl中替换SystemRoleMapper为ObjectPermissionApi调用
- 将验证产品角色的方法改为调用远程权限接口验证
- 更新ProductObjectPermissionService使用远程权限接口替代本地查询
- 修改ProductServiceImpl中权限获取逻辑使用新的对象权限API
- 移除原有的系统菜单和角色相关的数据对象依赖
- 在测试类中更新模拟对象和断言逻辑适配新的权限接口调用
2026-04-23 09:22:43 +08:00
2943a6255b docs(product): 删除产品管理SQL口径和业务设计文档
- 移除02-产品管理SQL已确认口径文档
- 移除02-产品管理业务设计文档
- 清理产品管理模块的详细设计说明
- 删除产品需求状态字段口径定义
- 移除来源承接与需求拆分口径说明
- 清理需求终态原因承接口径内容
- 删除产品生命周期管理设计
- 移除产品团队权限管理规范
- 清理产品与项目关系约束说明
- 删除轻量需求管理业务规则
- 移除产品状态机与流程设计
- 清理权限与动作矩阵定义
2026-04-22 18:18:38 +08:00
dk
f8231c2d51 feat(user): 支持前端用用户昵称字段进行模糊搜索
fix(post): 使岗位排序能按照sort字段来排序。
2026-04-22 14:38:41 +08:00
423 changed files with 38155 additions and 3286 deletions

115
.claude/settings.local.json Normal file
View File

@@ -0,0 +1,115 @@
{
"permissions": {
"allow": [
"Bash(git *)",
"Bash(cmd *)",
"PowerShell('--rdms-project--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project' | Sort-Object; '--rdms-system--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-system' | Sort-Object; '--rdms-framework--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-framework' | Sort-Object; '--rdms-gateway--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-gateway' | Sort-Object)",
"PowerShell('--docs--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\docs' -Recurse -Depth 2 | Sort-Object)",
"PowerShell('--project-boot src--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project' -Recurse -Depth 2 | Sort-Object)",
"PowerShell('--project-api src--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-api\\\\src\\\\main\\\\java' -Recurse -Depth 5 | Sort-Object)",
"PowerShell('--system-boot src--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-system\\\\rdms-system-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\system' -Recurse -Depth 2 | Sort-Object)",
"PowerShell('--project-boot service/project--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\service\\\\project' -Recurse -Depth 3 | Sort-Object)",
"PowerShell('--project-boot controller--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\controller' -Recurse -Depth 4 | Sort-Object)",
"PowerShell('--project-api full--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-api\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project' -Recurse | Sort-Object)",
"PowerShell('--project-boot dal--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\dal' -Recurse | Sort-Object)",
"PowerShell('--project-api enums--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-api\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\enums' -Recurse | Sort-Object; '--system-api full--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-system\\\\rdms-system-api\\\\src\\\\main\\\\java' -Recurse -Depth 6 | Sort-Object)",
"PowerShell('--project-boot framework--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\java\\\\com\\\\njcn\\\\rdms\\\\module\\\\project\\\\framework' -Recurse | Sort-Object)",
"PowerShell('--sql resources--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\resources' -Recurse | Sort-Object; '--system sql--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-system\\\\rdms-system-boot\\\\src\\\\main\\\\resources\\\\sql' -Recurse | Sort-Object)",
"PowerShell('--project-boot resources sql--'; Get-ChildItem -Force -Name 'C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\rdms-project\\\\rdms-project-boot\\\\src\\\\main\\\\resources\\\\sql' -Recurse | Sort-Object)",
"WebSearch",
"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 -Dtest=TaskAssigneeServiceImplTest test 2>&1 | Select-Object -Last 60)",
"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 -am -Dtest=TaskAssigneeServiceImplTest test 2>&1 | Select-Object -Last 80)",
"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 -am -Dtest=TaskAssigneeServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test 2>&1 | Select-Object -Last 80)",
"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 -am \"-Dtest=TaskAssigneeServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 80)",
"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 -am \"-Dtest=TaskAssigneeServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 50)",
"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 -am test 2>&1 | Select-Object -Last 80)",
"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 \"-Dtest=TaskAssigneeServiceImplTest,ProjectTaskServiceImplTest,ProjectTaskStatusViewServiceTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 30)",
"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 -am \"-Dtest=TaskAssigneeServiceImplTest,ProjectTaskServiceImplTest,ProjectTaskStatusViewServiceTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" test 2>&1 | Select-Object -Last 30)",
"Bash(grep -rn \"INSERT INTO \\\\`system_menu\\\\`\\\\|INSERT INTO system_menu\" --include=\"*.sql\" rdms-project rdms-system)",
"Bash(findstr /i \"plugin\")",
"Bash(findstr *)",
"Bash(awk END{print NR} *)",
"PowerShell(Move-Item *)",
"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 -am test '-Dtest=VisibilityScopeResolverImplTest' -q 2>&1 | Select-Object -Last 80)",
"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 -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' -q 2>&1 | Select-Object -Last 60)",
"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 -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' -q | Select-Object -Last 60)",
"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 -am test '-Dtest=VisibilityScopeResolverImplTest' '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|ERROR|FAIL' | Select-Object -Last 40)",
"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 -am test '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'Tests run|BUILD|ERROR|FAILED|FAIL' | Select-Object -Last 100)",
"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 -am test-compile '-Dsurefire.failIfNoSpecifiedTests=false' | Select-String -Pattern 'ERROR|BUILD|FAIL' | Select-Object -Last 40)",
"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)",
"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 项目)",
"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 compile -am -DskipTests 2>&1 | Select-Object -Last 20)",
"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 compile -am -DskipTests -f \"C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\pom.xml\" 2>&1 | Select-Object -Last 30)",
"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-compile -am 2>&1 | Select-Object -Last 40)",
"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 -am \"-Dtest=ProjectTaskServiceImplTest,ProjectExecutionServiceImplTest\" 2>&1 | Select-Object -Last 80)",
"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 -am \"-Dtest=ProjectTaskServiceImplTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" 2>&1 | Select-Object -Last 80)",
"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 -am \"-Dtest=ProjectTaskServiceImplTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" 2>&1 | Select-Object -First 200)",
"PowerShell(Get-ChildItem \"C:\\\\code\\\\gitea\\\\rdms\\\\cn-rdms\\\\.claude\\\\worktrees\\\\agent-a0979555dc2fe9384\\\\rdms-project\\\\rdms-project-boot\\\\target\\\\surefire-reports\" | Where-Object { $_.Name -match \"Test\" } | ForEach-Object { $_.Name })",
"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 compile -am -DskipTests 2>&1 | Select-Object -Last 30)",
"Bash(/c/software/apache-maven-3.8.9/bin/mvn.cmd -pl rdms-project/rdms-project-boot test -am -Dtest=ProjectTaskServiceImplTest#changeTaskStatus_shouldUseTransitionAndWriteLogs -q)",
"Bash(/c/software/apache-maven-3.8.9/bin/mvn.cmd -pl rdms-project/rdms-project-boot test -Dtest=ProjectTaskServiceImplTest#changeTaskStatus_shouldUseTransitionAndWriteLogs -Dsurefire.failIfNoSpecifiedTests=false -q)",
"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 \"-Dtest=ProjectTaskServiceImplTest#changeTaskStatus_shouldUseTransitionAndWriteLogs\" \"-Dsurefire.failIfNoSpecifiedTests=false\" -q 2>&1 | Select-Object -Last 60)",
"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-compile -am -q 2>&1 | Select-Object -Last 40)",
"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-compile -am -q 2>&1 | Select-Object -Last 20)",
"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-compile -am 2>&1 | Select-String \"BUILD SUCCESS|BUILD FAILURE|ERROR\" | Select-Object -Last 5)",
"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 -am \"-Dtest=ProjectTaskServiceImplTest,ProjectExecutionServiceImplTest\" \"-Dsurefire.failIfNoSpecifiedTests=false\" 2>&1 | Select-String \"Tests run|BUILD SUCCESS|BUILD FAILURE|FAILED|<<<\" | Select-Object -Last 30)",
"PowerShell([System.IO.Directory]::GetCurrentDirectory\\(\\))",
"Bash(Get-ChildItem -Directory -Name)",
"Bash(grep \"\\\\.java$\")",
"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 -am compile -DskipTests 2>&1 | Select-Object -Last 30)",
"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 -am clean compile -DskipTests 2>&1 | Select-Object -Last 35)",
"Bash(C:/Program Files/Java/jdk-17/bin/java.exe *)",
"Bash(export JAVA_HOME=\"C:/Program Files/Java/jdk-17\")",
"Bash(cd \"C:/code/gitea/rdms/cn-rdms\")",
"Bash(\"C:/software/apache-maven-3.8.9/bin/mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)",
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests | Select-Object -Last 15)",
"PowerShell(java *)",
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests -q 2>&1 | Select-Object -Last 30)",
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; $out = & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests 2>&1; $code = $LASTEXITCODE; $out | Select-Object -Last 15; Write-Output \"EXIT=$code\")",
"Bash(set \"JAVA_HOME=C:\\\\Program Files\\\\Java\\\\jdk-17\")",
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)",
"Bash(grep -E \"\\\\.\\(sql|java|md\\)$\")",
"Bash(xargs grep -l \"INSERT INTO.*system_menu\")",
"Bash(Get-ChildItem *)",
"Bash(Select-Object FullName)"
]
}
}

3
.gitignore vendored
View File

@@ -76,3 +76,6 @@ sessionStore
# local docs # local docs
/docs/ /docs/
# Claude Code 本地工作区
.claude/

View File

@@ -0,0 +1,4 @@
#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice.
#Tue Apr 21 08:50:58 CST 2026
https\://maven.aliyun.com/repository/public/.lastUpdated=1776732658408
https\://maven.aliyun.com/repository/public/.error=

230
AGENTS.md
View File

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

175
CLAUDE.md Normal file
View File

@@ -0,0 +1,175 @@
# CLAUDE.md
本文件为 `C:\code\gitea\rdms\cn-rdms` 仓库下所有 Agent含 Claude Code的常驻工作指引`AGENTS.md` 引用本文件。
## 工作方式
- 默认先给执行方案:目标、涉及模块、改动点、验证方式;不擅自动手。
- 用户只要分析或评审时,停在分析层;不要顺手开工。
- 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。
- **输出极简**:先给结论、改动点、必要风险;用自然语言给判断和影响面,少贴代码片段;涉及代码用 `file_path:line_number` 引用;用户追问再展开。
- **下定论需要充足证据**。疑似 bug 时先判断是否稳定复现:跑了很久没动过的功能**首次**报错优先怀疑运行时状态污染devtools / IDE 热替换、ApplicationContext 残留、缓存、Redis / DB 连接、JVM 静态字段被旧 context 设过等),**不要凭单次堆栈就断言代码 bug更不要直接甩修改方案**。先给"可能原因 + 最便宜的取证步骤"(多数场景是**冷重启 JVM**),用户确认能稳定复现,再讨论代码层面的修复。同款写法在仓库其它位置存在并不能反推"也是 bug",长期能跑的代码突然失效 ≠ 代码本身错。
- **技术风险判断(性能 / N+1 / 索引缺失 / 架构缺陷 / 并发安全 / 内存泄漏 等)与"bug 判断"同等严格**:未读到实现层不下结论。不要凭 subagent 摘要、字段名、注释或印象"顺嘴提一句风险/瓶颈/可能问题"——那也是下结论,**且杀伤力更大**:用户会基于"风险提示"决定要不要立项整改。如果当前上下文没核实到实现,就明说"这部分未核实,需要打开 X 文件确认",不要把猜测包装成"风险提示"塞出去。已识别教训:执行进度查询答完"已批量聚合无 N+1"后又凭印象抛"列表 N+1 风险",被追问才收回。
## 本机环境
- 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否则在该命令上下文中显式切换。
## 仓库结构
多模块 Maven 单仓库Java 17Spring Boot 3.5.9,根模块打包 `pom`
顶层模块:
1. `rdms-system` — 系统域(用户/组织/岗位/菜单/角色/权限)
2. `rdms-project` — RDMS 核心交付域(项目集/项目/产品/需求/任务/工单/执行)
3. `rdms-framework` — 共享框架与内部 starter
4. `rdms-gateway` — Spring Cloud Gateway 网关
每个业务模块按 `xxx-api` + `xxx-boot` 拆分:
- `*-api`:对外 RPC/Feign 接口、DTO、错误码、枚举、常量
- `*-boot`启动类、controller、service、dal、convert、api 实现、模块级 framework 配置
主包/启动类:
- `rdms-system-boot``com.njcn.rdms.module.system.SystemServerApplication`
- `rdms-project-boot``com.njcn.rdms.module.project.ProjectServerApplication`
- `rdms-gateway``com.njcn.rdms.gateway.GatewayServerApplication`
`rdms-framework` 子模块:`rdms-common` 及一组 `rdms-spring-boot-starter-*``env``web``rpc``security``mybatis``redis``mq``websocket``excel``protection``test``biz-ip`)。
## 模块演进判断
新增能力时**先判断落点**
- 落在现有 `rdms-system` 子域 → 沿用 `controller/admin|app``service``dal/dataobject``dal/mysql``convert` 的现有结构,跨模块暴露时在 `rdms-system-api` 补 API/DTO/错误码/枚举。
- 落在现有 `rdms-project` 子域 → 同理,跨模块走 `rdms-project-api`
- 已具备独立服务边界(如未来的 `rdms-workflow`)→ 新建 `rdms-xxx` + `rdms-xxx-api` + `rdms-xxx-boot`,根 `pom.xml` 加聚合,包路径 / `spring.application.name` / `ApiConstants` / `RpcConstants` / `rdms.info.base-package` 保持一致。
不要:
- 把后续业务长期堆进 `rdms-system`
- 为新增子域引入一套平行的 `application/domain/infrastructure/adapter` 分层。
- 让外部模块直接依赖 `*-boot` 的 service 或 mapper必须走 `*-api`)。
## 分层职责
| 层 | 职责 | 红线 |
|---|---|---|
| `rdms-framework` | 基础能力 | 不承载业务语义;除非框架级缺陷或全局基础设施,不要把业务判断塞进来 |
| `rdms-gateway` | 入口、令牌校验、登录用户透传、路由、横切 | 不要在这里承载组织/成员/负责人/项目/产品/工作流状态/数据可见性 |
| Controller | HTTP 暴露、参数校验、权限注解、结果封装 | 不要编排复杂业务流程,不要直接操作多个 mapper`ReqVO`/`RespVO`,不要直接暴露 DO |
| Service | 业务规则、事务、缓存、领域编排 | 已有领域优先扩展,不要为"整齐"平移;不要把规则散到 controller / mapper / util |
| DAL | DO + Mapper | Mapper 继承 `BaseMapperX<T>`;查询优先 `LambdaQueryWrapperX` 与默认方法封装;非必要不回退 XML不承担领域校验 |
| Convert | 已有 `convert` 风格继续沿用,简单场景直接 `BeanUtils` | 不要强推全员 MapStruct也不要反过来把已有 convert 全删 |
## 认证与跨模块调用
- 默认沿用 OAuth2 / Token / `LoginUser` / `login-user` 透传主链。**不要**另造 ThreadLocal / Session / 自定义 header。
- 业务逻辑落 `*-boot`;可复用契约落 `*-api`;可复用框架能力落 `rdms-framework`。跨模块/跨服务必须通过 `*-api` 定义契约,**不要直接依赖别人的 `*-boot`**;改跨模块 API 时,`*-boot` 实现与对应 `*-api` 契约同步更新。
### 鉴权:必须按"全域 / 对象域"分通道挂
系统有**两条互不交叉**的权限通道,挂错通道 = 永远 403。新增/修改接口前必须先判断它属于哪一域:
| 通道 | 适用场景 | 注解 / 实现 | 角色与菜单 |
|---|---|---|---|
| **全域 global** | 传统 RBAC 顶层菜单与"项目管理界面"——选择对象**之前**的所有动作(建项目、列项目、菜单/角色/用户管理等) | Controller 上 `@PreAuthorize("@ss.hasPermission('xxx')")`,由 `PermissionServiceImpl.hasAnyPermissions` 处理 | `system_role.scope_type='global'` + `system_menu.scope_type='global'` |
| **对象域 object** | 用户**已选择某个对象**(如某个项目/产品)后,对象内部的一切操作(任务、执行、工时、协办人、需求、成员维护等) | Service 上 `@CheckObjectPermission(objectType=..., objectId="#xxxId", permission="...")`,由 `ObjectPermissionAspect``ObjectPermissionService.checkPermission` 处理 | `system_role.scope_type='object'` + `system_menu.scope_type='object'` + `object_type` |
红线:
- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色line 343-347+ 强制按 GLOBAL 查菜单line 92-94对象域角色与对象域菜单都进不来即使授权配置完全正确也必然 403。
- **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
- 对象内**读路径**(列表 / 详情 / 状态看板 / 聚合)已统一在 **Service 层**挂 `@CheckObjectPermission(objectType=PROJECT, permission=...PERMISSION_QUERY)`——查询同样要扫库耗资源,必须按对象域鉴权(原台账 TD-001 所述"读路径未挂"已不成立)。**Controller 方法层一律不挂权限注解**,对象域鉴权全部落 Service新增读接口照此在 Service 层挂对象域权限,不要只在 Controller 留空、更不要误判"Controller 没注解 = 无鉴权"。
判定口诀:**URL 里有 `{projectId}` / `{productId}` 等对象 ID → 对象域;没有 → 全域**。
## 接口语义(HTTP 动词)
本仓库 update 类接口默认按 RESTful 标准用 HTTP 动词区分语义,前后端必须按下表对齐,避免"前端没传字段"和"前端想清空"在后端无法区分的歧义。
| 动词 | 语义 | 字段处理规则 |
|---|---|---|
| **PUT** | 全资源替换 | 前端必须把表单完整状态回传(读到的非必填字段也要原样回传)。后端按字段值落库:**有值=更新,`null`=清空**。DO 字段加 `@TableField(updateStrategy = FieldStrategy.ALWAYS)` 跳过全局 `NOT_NULL``null` 真的写库 |
| **PATCH** | 部分字段更新 | **本仓库暂不引入 PATCH 接口**。如果有"只改一两个字段"的需求,用专门的子动作接口(参考 `assignees/{id}/inactive``status` 这种语义化路径),不要在 update 接口里靠旁路标记(如 `clearXxx: true`)模拟 PATCH |
| **DELETE** | 资源删除 | 软删走全局 `deleted` 列,不需要参数 body |
红线:
- **不要在 update 类接口的 ReqVO 里加 `clearXxx: Boolean` 这种旁路标记**来模拟 PATCH —— 等于承认接口是"伪 PATCH",会让所有非必填字段都需要类似标记,长期污染 API 设计。需要部分更新就拆子动作接口。
- 新增 update 接口时,必须在 API 文档对应章节明示"PUT 全字段回传"约定;DO 上对允许 null 的字段补 `FieldStrategy.ALWAYS` 注解,并加注释说明语义来源(指向本节)。
- 历史接口若是稀疏 PATCH 风格(传 null = 不动),保留现状但不要拓展;遇到清空诉求时按 PUT 方向重构。
## 数据与 SQL
- 新表 DO 复用现有 `BaseDO` / 审计字段风格,不要再引一套审计基类(除非该表本身明确不需要逻辑删除)。
- **不要假设运行时自动数据库迁移**:依赖新表/新字段/新索引时,必须同步补 SQL 脚本与文档。
- SQL 放在目标模块 `src/main/resources/sql/...`,可审阅、可单独执行。
- 缓存/日志/审计变更优先沿用既有机制,不要绕开登录上下文与审计字段填充。
### 种子 SQL纯 SQL INSERT 雪花 ID 表)
`system_dict_type` / `system_dict_data` / `system_menu` 等历史表 id 由 MyBatis-Plus 雪花算法在 Java 层生成DDL 无 `AUTO_INCREMENT`。纯 SQL 路径(字典种子、菜单种子等)写 INSERT 必须显式提供 id否则 MySQL 报 `1364 - Field 'id' doesn't have a default value`
- **id 取值**`SET @new_id = (SELECT IFNULL(MAX(id), 0) + 1 FROM xxx_table);` 然后 INSERT `SELECT @new_id, ...`。雪花 ID 单调递增,`MAX+1` 落在已用区间之后,不会与未来 Java 生成的新雪花 ID 冲突。
- **多条连续 INSERT**:每条 INSERT **前重新取** `MAX+1`——不要用 `base+1 / +2 / +3` 一次性算多个。配合 `NOT EXISTS` 守卫,部分已存在场景(半路重跑)才不会出现两条共用一个 id。
- **collation 1267 陷阱**:仓库历史表 collation 不统一(如 `system_dict_data``utf8mb4_unicode_ci`,新表 `rdms_task_worklog``utf8mb4_0900_ai_ci`)。**不要**用 `SET @t = 'xxx'` 存字符串再 `WHERE col = @t`——用户变量自带连接级 collation与列 collation 撞会报 `1267 Illegal mix of collations (utf8mb4_unicode_ci,IMPLICIT) and (utf8mb4_0900_ai_ci,IMPLICIT) for operation '='`。**对策**直接展开成字面值MySQL 字面值会按列 collation 隐式解析,不冲突。
样板参考:`docs/sql/rdms_task_worklog.sql:47-50`(菜单种子)+ `docs/sql/rdms_worklog_difficulty_seed.sql`(字典种子)。
## 注释与编码
- 关键字段/分支/约束/非直观实现补**简洁中文**注释;中文写入必须 UTF-8不要用"改成英文"规避乱码。
- superpowers 产出的功能文档(设计/实施/联调默认中文落地代码标识、文件路径、接口路径、SQL、命令保持原样不意译。
## 文档输出格式
- 新写文档默认输出 **HTML 格式**(便于浏览器直接打开、自带样式)。
- 例外:`docs/superpowers/` 下保持 markdown工作流约定
- 历史已有的 markdown 文档不强制迁移;只有新写的按 HTML。
## 工作规则(执行前对照)
1. 优先做有边界的模块内改动,避免跨模块扩散。
2. **不要修改** `application-local.yaml` / `application-dev.yaml`,除非任务本身就是环境配置调整;改前先查 git 状态。
3. 新增共享能力优先扩展现有 `rdms-spring-boot-starter-*`,不要在业务服务里重复堆配置。
4. **未经用户明确同意,不执行任何 `mvn`、启动命令、脚本等会实际运行项目的命令。**
## Git 操作纪律
### 默认不引导分支管理(**首要**
用户在本仓库长期固定在 `main` 上工作。开发流程中:
- **不要主动建议建 feature 分支**`git checkout -b feat/xxx``git switch -c ...`)。
- **不要把"先切到 xxx 分支再操作"作为方案前置步骤**。
- 一切围绕 `main` 展开:直接在 `main` 上改、`main` 上提交、`main` 上推。
- 例外:用户明确要求建分支、或涉及多人协作 / PR 评审 / 大规模重构(此时仍只是"提一句作为可选",不强推)。
**理由**:这次差点丢用户 3 天工作的事故,根因就是分支管理本身——某次操作意外把文件名当成分支名(建出 `用户行动清单.md` 分支),后续"切回 main + `git branch -D` 删怪分支"流程里就把未推送 commit `8bad989` 干掉了。**少走分支 = 少埋雷**。
### 破坏性 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 <ref>``git log <主线>..<ref>` 把输出贴回来。
2. **工作区是否干净**`git status`
3. **先挂救生圈**:建议用 `git tag backup-xxx <sha>` 锁定当前 SHA**再执行**破坏性命令。
4. **明示翻车回滚路径**:例如"如果不对,`git reset --hard backup-xxx` 即可回到此处"。
## 验证默认动作
先定义验证方式,再做修改。默认静态验证:
- 调用链是否闭环、是否符合模块边界
- 配置项 / 接口契约 / 权限标识 / 路由 / 资源注册前后是否一致
- 改动是否控制在最小集合
- 文档 / SQL / 配置 / 接口说明是否需要同步更新
如果改动涉及 Spring 配置、序列化、安全、路由、RPC 契约、MyBatis 行为或跨模块 API必须**显式区分**哪些是已静态检查、哪些尚未实际运行验证。
## 给后续我自己的提醒
- 仓库可能有未提交的本地改动,不要顺手覆盖与当前任务无关的编辑。
- `docs/` 是当前工作上下文的一部分,不是归档;架构级修改前先查阅 [`docs/README.md`](./docs/README.md)。
-`pom.xml` 统一版本与依赖;版本调整改根 pom不要散落到子模块。
- 推荐使用 `Glob` / `Grep` / `Read` 等专用工具,避免用 Bash 做文件搜索/读取/编辑。

View File

@@ -0,0 +1,355 @@
# 工单需求规格说明
日期2026-05-22
## 1. 背景
`rdms-project` 当前承载项目、产品、需求、执行、任务等核心交付对象。现有代码中产品需求、项目需求已经具备 `sourceType` / `sourceBizId` 来源字段,可以承接来自工单的需求派生关系;执行和任务也已经形成“项目需求 -> 执行 -> 任务”的后续交付链路。
本需求新增内部工单能力。工单作为独立业务对象存在,不复用需求、执行或任务主表。工单用于记录内部用户提交的诉求,经工单负责人受理后,可按归属类型派生产品需求或项目需求,并通过现有需求链路继续流转到执行、任务。
## 2. 目标
1. 支持内部用户创建普通工单或父工单。
2. 支持父工单逐步拆分子工单,父工单只汇总,不直接处理。
3. 支持普通工单、子工单作为最小处理单位,由指定工单负责人受理、拒绝、处理和关闭。
4. 支持工单单归属到一个产品或一个项目。
5. 支持产品工单派生产品需求、项目工单派生项目需求。
6. 支持有派生需求的工单在全部需求完成后自动关闭。
7. 支持无派生需求的工单由工单负责人手动关闭。
## 3. 非目标
1. 本期不做外部客户工单,不保留 `sourceChannel``externalCustomerName``externalContact` 等外部来源字段。
2. 本期不做工单编号 `ticketNo`
3. 工单不能直接派生执行或任务。
4. 父工单不能受理、拒绝、派生需求或手动关闭。
5. 本期不引入流程引擎,不做可配置审批流。
6. 本期不自动判断工单是否涉及多个产品/项目,也不自动判断归属产品或项目;这些由录入人员人工选择。
## 4. 核心概念
### 4.1 工单形态
使用 `ticketMode` 表达工单形态:
| 值 | 含义 | 是否可处理 | 说明 |
|---|---|---|---|
| `single` | 普通工单 | 是 | 单归属工单,不挂父工单 |
| `parent` | 父工单 | 否 | 原始诉求汇总单,可持续拆分子工单 |
| `child` | 子工单 | 是 | 挂在父工单下的最小处理单位 |
不使用 `isParent``ticketMode` 比布尔字段更准确,可以区分普通工单、父工单和子工单,避免在父工单尚未创建子工单时无法识别其形态。
### 4.2 归属类型
普通工单和子工单必须单归属:
| `belongType` | 归属对象 | 可派生对象 | 后续链路 |
|---|---|---|---|
| `product` | 一个产品 | 产品需求 | 产品需求 -> 指派项目 -> 项目需求 -> 执行 -> 任务 |
| `project` | 一个项目 | 项目需求 | 项目需求 -> 执行 -> 任务 |
父工单不填写 `belongType``productId``projectId`
## 5. 业务流程
### 5.1 总流程图
```mermaid
flowchart TD
A([开始]) --> B[录入人员创建工单]
B --> C{ticketMode}
C -->|parent| P1[父工单: splitting]
P1 --> P2[列表操作列拆分子工单]
P2 --> C1[创建子工单: pending_accept]
P1 --> P3{是否存在子工单且全部为终态}
P3 -->|否| P1
P3 -->|是| P4[父工单: closed]
P4 --> Z([结束])
C -->|single| S1[普通工单: pending_accept]
C1 --> S2[工单负责人待处理]
S1 --> S2
S2 --> D{是否受理}
D -->|否| R[工单: rejected]
R --> Z
D -->|是| E[工单: processing]
E --> F{是否派生需求}
F -->|否| G[负责人填写处理结论并手动关闭]
G --> H[工单: closed]
H --> I[触发父工单汇总检查]
I --> Z
F -->|是| J{归属类型}
J -->|product| K[派生一个或多个产品需求]
J -->|project| L[派生一个或多个项目需求]
K --> M[等待派生需求全部完成]
L --> M
M --> N{全部派生需求完成}
N -->|否| E
N -->|是| O[系统自动关闭工单: closed]
O --> I
```
### 5.2 父工单流程
1. 录入人员创建父工单,状态为 `splitting`
2. 父工单只记录原始诉求和附件,不进入处理队列。
3. 工单列表查询的操作列为父工单提供“拆分子工单”入口。
4. 录入人员可以持续新增子工单。
5. 父工单至少存在一个子工单,且所有子工单均进入终态后,系统自动关闭父工单。
6. 父工单关闭后不再作为处理对象,但可继续作为历史汇总查看。
### 5.3 普通工单 / 子工单流程
1. 创建后进入 `pending_accept`
2. 工单负责人判断是否受理。
3. 不受理则进入 `rejected`,需要填写拒绝原因。
4. 受理后进入 `processing`
5. 处理中可以派生需求,也可以在无派生需求时填写处理结论并手动关闭。
6. 一旦存在派生需求,工单不能手动关闭,必须等待全部派生需求完成后自动关闭。
## 6. 状态模型
### 6.1 父工单状态
| 状态 | 含义 | 进入方式 | 退出方式 |
|---|---|---|---|
| `splitting` | 拆分中 / 汇总中 | 创建父工单 | 所有子工单终态后自动关闭 |
| `closed` | 已关闭 | 系统自动关闭 | 终态 |
父工单不允许进入 `pending_accept``rejected``processing`
### 6.2 普通工单 / 子工单状态
| 状态 | 含义 | 进入方式 | 退出方式 |
|---|---|---|---|
| `pending_accept` | 待受理 | 创建普通工单或子工单 | 受理或拒绝 |
| `rejected` | 已拒绝 | 工单负责人拒绝 | 终态 |
| `processing` | 处理中 | 工单负责人受理 | 手动关闭或自动关闭 |
| `closed` | 已关闭 | 手动关闭或派生需求全部完成后自动关闭 | 终态 |
终态包括 `rejected``closed`
### 6.3 自动关闭规则
1. 普通工单 / 子工单存在派生需求时,只有全部派生需求完成后才自动关闭。
2. 普通工单 / 子工单不存在派生需求时,允许工单负责人手动关闭,必须填写 `closeResult`
3. 父工单至少存在一个子工单,且所有子工单均为 `rejected``closed` 后,自动关闭。
4. 父工单不直接检查需求完成情况,只汇总子工单终态。
## 7. 数据模型
### 7.1 工单主表
建议表名:`rdms_ticket`
| 字段 | 类型建议 | 必填规则 | 说明 |
|---|---|---|---|
| `id` | `bigint` | 是 | 主键 |
| `parent_id` | `bigint` | 否 | 父工单 ID`child` 必填 |
| `ticket_mode` | `varchar(32)` | 是 | `single` / `parent` / `child` |
| `title` | `varchar(255)` | 是 | 工单标题 |
| `description` | `text` | 否 | 工单描述,支持富文本 |
| `ticket_type` | `varchar(32)` | 是 | 工单类型,字典 |
| `priority` | `varchar(32)` | 否 | 优先级,建议复用需求优先级字典 |
| `status_code` | `varchar(32)` | 是 | 工单状态 |
| `submitter_id` | `bigint` | 是 | 提交人用户 ID |
| `submitter_nickname` | `varchar(64)` | 否 | 提交人昵称快照 |
| `owner_id` | `bigint` | `single` / `child` 必填 | 工单负责人用户 ID |
| `owner_nickname` | `varchar(64)` | 否 | 工单负责人昵称快照 |
| `belong_type` | `varchar(32)` | `single` / `child` 必填 | `product` / `project` |
| `product_id` | `bigint` | 产品工单必填 | 归属产品 ID |
| `project_id` | `bigint` | 项目工单必填 | 归属项目 ID |
| `accept_time` | `datetime` | 否 | 受理时间 |
| `reject_reason` | `varchar(500)` | 拒绝时必填 | 拒绝原因 |
| `close_time` | `datetime` | 否 | 关闭时间 |
| `close_result` | `varchar(1000)` | 手动关闭时必填 | 处理结论 |
| `attachments` | `json` | 否 | 附件列表,沿用 `AttachmentItem` |
审计字段、逻辑删除字段复用现有 `BaseDO` 风格。
### 7.2 工单需求关联表
建议表名:`rdms_ticket_requirement_link`
| 字段 | 类型建议 | 必填规则 | 说明 |
|---|---|---|---|
| `id` | `bigint` | 是 | 主键 |
| `ticket_id` | `bigint` | 是 | 工单 ID |
| `target_type` | `varchar(32)` | 是 | `product_requirement` / `project_requirement` |
| `target_id` | `bigint` | 是 | 需求 ID |
不设置 `relationType`。本期关联表只表达“该工单派生了哪些需求”。
建议唯一约束:`ticket_id + target_type + target_id`
## 8. 需求派生规则
### 8.1 产品工单派生产品需求
适用条件:
1. `ticketMode``single``child`
2. `belongType = product`
3. 工单状态为 `processing`
派生结果:
1. 创建 `ProductRequirementDO`
2. `productId` 使用工单 `productId`
3. `sourceType = "work_order"`
4. `sourceBizId = ticketId`
5. 标题、描述、优先级、附件、提出人等字段可从工单带入,并允许派生表单二次编辑。
6. 写入 `rdms_ticket_requirement_link``targetType = product_requirement`
后续链路沿用现有产品需求分发到项目的能力。
### 8.2 项目工单派生项目需求
适用条件:
1. `ticketMode``single``child`
2. `belongType = project`
3. 工单状态为 `processing`
派生结果:
1. 创建 `ProjectRequirementDO`
2. `projectId` 使用工单 `projectId`
3. `sourceType = "work_order"`
4. `sourceBizId = ticketId`
5. 标题、描述、优先级、附件、提出人等字段可从工单带入,并允许派生表单二次编辑。
6. 写入 `rdms_ticket_requirement_link``targetType = project_requirement`
后续执行和任务沿用现有项目需求、执行、任务链路。
### 8.3 完成判定
1. 产品工单只检查由该工单派生的产品需求。
2. 项目工单只检查由该工单派生的项目需求。
3. 需求完成态应复用现有需求状态模型的终态配置,不在工单逻辑中写死具体状态码。
4. 派生需求数量大于 0 且全部完成时,系统自动关闭对应工单。
## 9. 页面与待办
### 9.1 我的提交
展示当前用户提交的工单,包括:
1. 父工单。
2. 普通工单。
3. 子工单。
建议支持按状态、工单类型、归属类型、归属对象、创建时间筛选。
### 9.2 我的待处理
展示当前用户负责的普通工单和子工单,不展示父工单。
筛选条件:
1. `ownerId = 当前用户`
2. `ticketMode in (single, child)`
3. `statusCode in (pending_accept, processing)`
### 9.3 父工单列表操作
工单列表查询中,父工单行需要在操作列展示“拆分子工单”入口。该入口只对父工单提交人或具备工单管理权限的用户可见。
点击后进入新增子工单表单,表单需要携带父工单上下文,并要求录入人员填写子工单的归属类型、归属产品/项目、工单负责人、工单类型等处理字段。
### 9.4 父工单汇总展示
父工单汇总展示需要包含:
1. 原始诉求信息。
2. 子工单列表。
3. 子工单归属产品/项目。
4. 子工单负责人。
5. 子工单状态。
6. 子工单派生需求数量与完成数量。
7. 父工单自动关闭结果。
## 10. 权限规则
1. 创建工单走全域权限。
2. 父工单新增子工单:父工单提交人或具备工单管理权限的用户可操作。
3. 普通工单 / 子工单受理、拒绝、手动关闭:工单负责人或具备工单管理权限的用户可操作。
4. 派生产品需求时,需要满足产品对象权限。
5. 派生项目需求时,需要满足项目对象权限。
6. 父工单不能执行受理、拒绝、派生需求、手动关闭动作。
## 11. 接口建议
接口路径建议落在 `rdms-project` 模块:
| 方法 | 路径 | 说明 |
|---|---|---|
| `POST` | `/project/tickets` | 创建普通工单或父工单 |
| `POST` | `/project/tickets/{parentId}/children` | 父工单新增子工单 |
| `GET` | `/project/tickets/my-submitted/page` | 我的提交 |
| `GET` | `/project/tickets/my-pending/page` | 我的待处理 |
| `GET` | `/project/tickets/{id}` | 工单详情 |
| `POST` | `/project/tickets/{id}/accept` | 受理 |
| `POST` | `/project/tickets/{id}/reject` | 拒绝 |
| `POST` | `/project/tickets/{id}/close` | 无派生需求时手动关闭 |
| `POST` | `/project/tickets/{id}/derive-product-requirement` | 派生产品需求 |
| `POST` | `/project/tickets/{id}/derive-project-requirement` | 派生项目需求 |
创建、更新类接口需要继续遵守仓库 HTTP 动词语义约定。部分动作使用语义化 `POST` 子动作接口,不引入 `PATCH`
## 12. 校验规则
1. `ticketMode = parent` 时,`ownerId``belongType``productId``projectId` 必须为空。
2. `ticketMode = single` 时,`ownerId``belongType` 必填,且 `productId` / `projectId` 按归属类型二选一。
3. `ticketMode = child` 时,`parentId``ownerId``belongType` 必填,且父工单必须存在且 `ticketMode = parent`
4. 产品工单必须填写 `productId`,不能填写 `projectId`
5. 项目工单必须填写 `projectId`,不能填写 `productId`
6. 只有 `pending_accept` 状态的普通工单 / 子工单可以受理或拒绝。
7. 只有 `processing` 状态的普通工单 / 子工单可以派生需求。
8. 有派生需求的工单不能手动关闭。
9. 无派生需求的 `processing` 工单可以手动关闭,必须填写 `closeResult`
10. 父工单不能手动关闭。
11. 父工单至少有一个子工单,且全部子工单终态后才能自动关闭。
## 13. 错误处理
建议新增明确错误码覆盖以下场景:
1. 工单不存在。
2. 工单形态非法。
3. 父工单不能处理。
4. 子工单父级非法。
5. 工单归属类型非法。
6. 产品工单不能派生项目需求。
7. 项目工单不能派生产品需求。
8. 工单状态不允许当前动作。
9. 有派生需求的工单不能手动关闭。
10. 派生需求未全部完成,工单不能自动关闭。
## 14. 测试重点
1. 创建父工单后状态为 `splitting`,且不能受理、拒绝、派生需求、手动关闭。
2. 父工单可以持续新增多个子工单。
3. 子工单必须单归属产品或项目。
4. 普通工单 / 子工单创建后进入 `pending_accept`
5. 工单负责人拒绝后进入 `rejected`
6. 工单负责人受理后进入 `processing`
7. 无派生需求的 `processing` 工单可手动关闭。
8. 有派生需求的工单不能手动关闭。
9. 产品工单只能派生产品需求。
10. 项目工单只能派生项目需求。
11. 派生需求全部完成后自动关闭工单。
12. 所有子工单终态后自动关闭父工单。
## 15. 风险与约束
1. 自动关闭依赖需求完成态判定,实施时必须和现有需求状态模型对齐。
2. 产品需求分发到项目后的项目需求、执行、任务链路不属于工单直接职责,工单只追踪自己直接派生的需求。
3. 父工单允许持续补子工单,会带来“父工单已关闭后是否允许继续补子单”的边界。本规格默认父工单关闭后不再补子单;如需重开父工单,需要单独设计重开动作。
4. 现有前端已有 `/ticket/my-submitted``/ticket/my-pending` 资源入口,后端接口落地时需要与前端路由和菜单权限同步。

View File

@@ -29,6 +29,7 @@
<spring.boot.version>3.5.9</spring.boot.version> <spring.boot.version>3.5.9</spring.boot.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<swagger.version>2.2.38</swagger.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>

View File

@@ -72,7 +72,8 @@
<!-- Swagger 注解,用于 API 文档生成(@Schema、@Operation 等) --> <!-- Swagger 注解,用于 API 文档生成(@Schema、@Operation 等) -->
<dependency> <dependency>
<groupId>io.swagger.core.v3</groupId> <groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId> <artifactId>swagger-annotations-jakarta</artifactId>
<version>${swagger.version}</version>
</dependency> </dependency>
<!-- RPC 远程调用相关 --> <!-- RPC 远程调用相关 -->

View File

@@ -27,9 +27,8 @@ public class PageParam implements Serializable {
@Min(value = 1, message = "页码最小值为 1") @Min(value = 1, message = "页码最小值为 1")
private Integer pageNo = PAGE_NO; private Integer pageNo = PAGE_NO;
@Schema(description = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @Schema(description = "每页条数,最大值为 200;传 -1 表示不分页(查询全部)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "每页条数不能为空") @NotNull(message = "每页条数不能为空")
@Min(value = 1, message = "每页条数最小值为 1")
@Max(value = 200, message = "每页条数最大值为 200") @Max(value = 200, message = "每页条数最大值为 200")
private Integer pageSize = PAGE_SIZE; private Integer pageSize = PAGE_SIZE;

View File

@@ -10,6 +10,8 @@ import java.util.List;
public class InDictCollectionValidator implements ConstraintValidator<InDict, Collection<?>> { public class InDictCollectionValidator implements ConstraintValidator<InDict, Collection<?>> {
private static final String MISSING_DICT_DATA_MESSAGE = "字典数据缺失,请联系管理员维护";
private String dictType; private String dictType;
@Override @Override
@@ -25,6 +27,12 @@ public class InDictCollectionValidator implements ConstraintValidator<InDict, Co
} }
// 校验全部通过 // 校验全部通过
List<String> dbValues = DictFrameworkUtils.getDictDataValueList(dictType); List<String> dbValues = DictFrameworkUtils.getDictDataValueList(dictType);
if (dbValues.isEmpty()) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(MISSING_DICT_DATA_MESSAGE)
.addConstraintViolation();
return false;
}
boolean match = list.stream().allMatch(v -> dbValues.stream() boolean match = list.stream().allMatch(v -> dbValues.stream()
.anyMatch(dbValue -> dbValue.equalsIgnoreCase(v.toString()))); .anyMatch(dbValue -> dbValue.equalsIgnoreCase(v.toString())));
if (match) { if (match) {
@@ -40,4 +48,3 @@ public class InDictCollectionValidator implements ConstraintValidator<InDict, Co
} }
} }

View File

@@ -9,6 +9,8 @@ import java.util.List;
public class InDictValidator implements ConstraintValidator<InDict, Object> { public class InDictValidator implements ConstraintValidator<InDict, Object> {
private static final String MISSING_DICT_DATA_MESSAGE = "字典数据缺失,请联系管理员维护";
private String dictType; private String dictType;
@Override @Override
@@ -24,6 +26,12 @@ public class InDictValidator implements ConstraintValidator<InDict, Object> {
} }
// 校验通过 // 校验通过
final List<String> values = DictFrameworkUtils.getDictDataValueList(dictType); final List<String> values = DictFrameworkUtils.getDictDataValueList(dictType);
if (values.isEmpty()) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(MISSING_DICT_DATA_MESSAGE)
.addConstraintViolation();
return false;
}
boolean match = values.stream().anyMatch(v -> StrUtil.equalsIgnoreCase(v, value.toString())); boolean match = values.stream().anyMatch(v -> StrUtil.equalsIgnoreCase(v, value.toString()));
if (match) { if (match) {
return true; return true;
@@ -38,4 +46,3 @@ public class InDictValidator implements ConstraintValidator<InDict, Object> {
} }
} }

View File

@@ -99,7 +99,8 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> StrUtil.isEmpty(EnvUtils.getTag(instance))); List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> StrUtil.isEmpty(EnvUtils.getTag(instance)));
// 【重要】补充说明:如果希望在 chooseInstances 为空时,不允许打到有 tag 的实例,可以取消注释下面的代码 // 【重要】补充说明:如果希望在 chooseInstances 为空时,不允许打到有 tag 的实例,可以取消注释下面的代码
if (CollUtil.isEmpty(chooseInstances)) { if (CollUtil.isEmpty(chooseInstances)) {
log.warn("[filterTagServiceInstances][serviceId({}) 没有不带 tag 的服务实例列表,直接使用所有服务实例列表]", serviceId); // 本地开发场景下所有实例都带 tagHOSTNAMEfallback 到全集属于设计内常态、请求并未失败,降级为 debug 避免噪音
log.debug("[filterTagServiceInstances][serviceId({}) 没有不带 tag 的服务实例列表,直接使用所有服务实例列表]", serviceId);
chooseInstances = instances; chooseInstances = instances;
} }
return chooseInstances; return chooseInstances;
@@ -108,7 +109,8 @@ public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
// 情况二,有 tag 时,使用 tag 匹配服务实例 // 情况二,有 tag 时,使用 tag 匹配服务实例
List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance))); List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance)));
if (CollUtil.isEmpty(chooseInstances)) { if (CollUtil.isEmpty(chooseInstances)) {
log.warn("[filterTagServiceInstances][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag); // 同上:未命中 tag 时 fallback 到全集是设计内正常路径,降级为 debug
log.debug("[filterTagServiceInstances][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
chooseInstances = instances; chooseInstances = instances;
} }
return chooseInstances; return chooseInstances;

View File

@@ -14,6 +14,7 @@ import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.gateway.util.SecurityFrameworkUtils; import com.njcn.rdms.gateway.util.SecurityFrameworkUtils;
import com.njcn.rdms.gateway.util.WebFrameworkUtils; import com.njcn.rdms.gateway.util.WebFrameworkUtils;
import com.njcn.rdms.module.system.enums.ErrorCodeConstants; import com.njcn.rdms.module.system.enums.ErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction; import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.GlobalFilter;
@@ -25,10 +26,12 @@ import reactor.core.publisher.Mono;
import java.time.Duration; import java.time.Duration;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
@Slf4j
@Component @Component
public class TokenAuthenticationFilter implements GlobalFilter, Ordered { public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
@@ -37,6 +40,18 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
private static final LoginUser LOGIN_USER_EMPTY = new LoginUser(); private static final LoginUser LOGIN_USER_EMPTY = new LoginUser();
/**
* 跳过 access token 校验的路径白名单。
* 这些接口在 system 端标注 @PermitAll本就不需要登录态若前端调用时带过期 access
* 网关不应在此处拦截 1002023000否则 /refresh-token 永远走不到 system 的 1002023001 / 业务逻辑。
*/
private static final Set<String> SKIP_AUTH_PATHS = Set.of(
"/admin-api/system/auth/login",
"/admin-api/system/auth/logout",
"/admin-api/system/auth/refresh-token",
"/admin-api/system/auth/register"
);
private final WebClient webClient; private final WebClient webClient;
private final LoadingCache<String, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1), private final LoadingCache<String, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1),
@@ -44,8 +59,16 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
@Override @Override
public LoginUser load(String token) { public LoginUser load(String token) {
String body = checkAccessToken(token).block(); // 仅异步 refresh 走这里(同步链路用 getIfPresent + 直接 checkAccessToken不触发 load
return buildUser(body, token); // 远端 token 已过期/校验失败时吞掉 ServiceException
// 若抛出,会被 Guava 包成 ExecutionException 并由刷新线程池作为 UncaughtException 打到日志,看起来像故障。
try {
String body = checkAccessToken(token).block();
return buildUser(body, token);
} catch (ServiceException ex) {
log.info("[loginUserCache] 异步刷新忽略 token 校验失败code={}, msg={}", ex.getCode(), ex.getMessage());
return LOGIN_USER_EMPTY;
}
} }
}); });
@@ -58,6 +81,11 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange = SecurityFrameworkUtils.removeLoginUser(exchange); exchange = SecurityFrameworkUtils.removeLoginUser(exchange);
// 白名单路径直接放行,不做 token 校验
if (SKIP_AUTH_PATHS.contains(exchange.getRequest().getPath().value())) {
return chain.filter(exchange);
}
String token = SecurityFrameworkUtils.obtainAuthorization(exchange); String token = SecurityFrameworkUtils.obtainAuthorization(exchange);
if (StrUtil.isEmpty(token)) { if (StrUtil.isEmpty(token)) {
return chain.filter(exchange); return chain.filter(exchange);

View File

@@ -6,10 +6,10 @@ spring:
username: # Nacos 账号 username: # Nacos 账号
password: # Nacos 密码 password: # Nacos 密码
discovery: # 【配置中心】配置项 discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境 namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项 config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境 namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 监控相关配置 #################### #################### 监控相关配置 ####################

View File

@@ -6,10 +6,10 @@ spring:
username: # Nacos 账号 username: # Nacos 账号
password: # Nacos 密码 password: # Nacos 密码
discovery: # 【配置中心】配置项 discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境 namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项 config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境 namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
#################### 监控相关配置 #################### #################### 监控相关配置 ####################

View File

@@ -30,6 +30,15 @@ spring:
gateway: gateway:
server: server:
webflux: webflux:
# HttpClient 连接池配置:网关作为反向代理客户端,复用到下游服务的 keep-alive 连接。
# 必须保证 max-idle-time < 下游 server.tomcat.keep-alive-timeout当前下游为 60s
# 否则服务端先 FIN、网关池仍持有"已死连接",复用时会抛 reactor.netty.http.client.PrematureCloseException。
httpclient:
connect-timeout: 10000 # 建立连接超时,毫秒
response-timeout: 30s # 接收响应超时
pool:
max-idle-time: 30s # 闲置连接最长保留 30s严格小于下游 keep-alive-timeout(60s)
evict-in-background: 60s # 周期后台驱逐过期连接,进一步降低 race 概率
# 路由配置项,对应 RouteDefinition 数组 # 路由配置项,对应 RouteDefinition 数组
routes: routes:
## system-server 服务 ## system-server 服务

View File

@@ -24,7 +24,8 @@
<!-- Web 相关 --> <!-- Web 相关 -->
<dependency> <dependency>
<groupId>io.swagger.core.v3</groupId> <groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId> <artifactId>swagger-annotations-jakarta</artifactId>
<version>${swagger.version}</version>
</dependency> </dependency>
<!-- 参数校验 --> <!-- 参数校验 -->

View File

@@ -4,7 +4,7 @@ import com.njcn.rdms.framework.common.exception.ErrorCode;
/** /**
* Project 错误码枚举类 * Project 错误码枚举类
* * <p>
* 产品管理当前使用 1-008-001-000 段。 * 产品管理当前使用 1-008-001-000 段。
*/ */
public interface ErrorCodeConstants { public interface ErrorCodeConstants {
@@ -18,6 +18,226 @@ public interface ErrorCodeConstants {
ErrorCode PRODUCT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_001_005, "动作【{}】必须填写原因"); 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_DELETE_NAME_MISMATCH = new ErrorCode(1_008_001_006, "删除确认名称与当前产品名称不一致");
ErrorCode PRODUCT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_001_007, "当前产品状态不允许编辑"); ErrorCode PRODUCT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_001_007, "当前产品状态不允许编辑");
ErrorCode PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE = new ErrorCode(1_008_001_008, "产品暂停后仅允许变更产品经理、描述和备注"); ErrorCode PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE = new ErrorCode(1_008_001_008, "产品暂停后仅允许修正描述,产品经理请通过产品团队维护");
ErrorCode PRODUCT_MEMBER_NOT_EXISTS = new ErrorCode(1_008_001_009, "产品团队成员不存在");
ErrorCode PRODUCT_MEMBER_ALREADY_EXISTS = new ErrorCode(1_008_001_010, "该用户已是当前产品的有效团队成员");
ErrorCode PRODUCT_MEMBER_ROLE_INVALID = new ErrorCode(1_008_001_011, "角色不存在或不属于产品对象角色");
ErrorCode PRODUCT_MANAGER_TRANSFER_INFO_REQUIRED = new ErrorCode(1_008_001_013, "切换产品经理时必须同时传入原产品经理用户和交接后角色");
ErrorCode PRODUCT_MANAGER_MEMBER_NOT_ALLOW_REMOVE = new ErrorCode(1_008_001_014, "当前产品经理不能移出产品团队,请先完成经理转交");
ErrorCode PRODUCT_MANAGER_MEMBER_NOT_ALLOW_DOWNGRADE = new ErrorCode(1_008_001_015, "当前产品经理不能直接调整为非经理角色,请先完成经理转交");
ErrorCode PRODUCT_MEMBER_NOT_ACTIVE = new ErrorCode(1_008_001_016, "当前产品团队成员已失效");
ErrorCode PRODUCT_MANAGER_TRANSFER_SOURCE_INVALID = new ErrorCode(1_008_001_017, "原产品经理信息与当前产品经理不一致");
ErrorCode PRODUCT_MANAGER_TRANSFER_ROLE_INVALID = new ErrorCode(1_008_001_018, "原产品经理交接后的角色不能仍为产品经理");
ErrorCode PRODUCT_MANAGER_NOT_MODIFIABLE = new ErrorCode(1_008_001_019, "产品主数据编辑不允许直接变更产品经理,请通过产品团队维护");
ErrorCode PRODUCT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_001_020, "当前用户不具备该产品的操作权限【{}】");
ErrorCode PRODUCT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_001_021, "删除确认口令不正确");
ErrorCode PRODUCT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_001_022, "产品状态已发生变化,请刷新后重试");
ErrorCode PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_001_023, "产品状态定义不存在或已停用");
// 产品创建原子接口(/create-with-team专用初始团队相关校验
ErrorCode PRODUCT_INITIAL_TEAM_MANAGER_REQUIRED = new ErrorCode(1_008_001_024, "初始团队必须包含产品经理");
ErrorCode PRODUCT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_001_025, "初始团队成员存在重复");
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, "产品需求不存在");
ErrorCode REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_001, "当前需求状态不支持动作【{}】");
ErrorCode REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_002, "动作【{}】必须填写原因");
ErrorCode REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_003, "需求状态已发生变化,请刷新后重试");
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_004, "当前需求状态不允许编辑");
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_005, "只有已验收的需求才能关闭");
ErrorCode REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_006, "需求状态定义不存在或已停用");
ErrorCode REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_014, "只有待认领、待评审、待指派状态的需求才能删除");
ErrorCode REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_007, "父需求状态不是已评审、待指派、实施中,不允许拆分");
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_002_008, "存在未处理完的子需求,请先处理子需求");
ErrorCode REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_002_017, "只有不存在子需求,或子需求都处于已取消和已拒绝的状态才能取消");
ErrorCode REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_002_013, "存在子需求,请先删除子需求");
ErrorCode REQUIREMENT_MODULE_NOT_EXISTS = new ErrorCode(1_008_002_009, "需求模块不存在");
ErrorCode REQUIREMENT_MODULE_NAME_DUPLICATE = new ErrorCode(1_008_002_010, "已经存在名称为【{}】的模块");
ErrorCode REQUIREMENT_MODULE_NOT_BELONG_TO_PRODUCT = new ErrorCode(1_008_002_011, "模块不属于当前产品");
ErrorCode REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS = new ErrorCode(1_008_002_012, "模块下存在非终态需求,不可删除");
ErrorCode REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_002_015, "存在子模块,请先删除子模块");
ErrorCode REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_002_016, "模块下存在需求,请先删除需求");
ErrorCode REQUIREMENT_PROJECT_MODULE_ROOT_NOT_EXISTS = new ErrorCode(1_008_002_018, "关联项目下不存在根模块,请先创建项目根模块");
ErrorCode REQUIREMENT_DISPATCHED_NOT_ALLOW_SPLIT = new ErrorCode(1_008_002_019, "产品需求已指派生成项目需求,不允许再在产品端拆分");
ErrorCode REQUIREMENT_NOT_DISPATCHED = new ErrorCode(1_008_002_020, "该产品需求尚未指派到关联项目");
ErrorCode REQUIREMENT_DISPATCHED_PROJECT_REQUIREMENT_NOT_FOUND = new ErrorCode(1_008_002_021, "未找到该产品需求对应的项目需求");
ErrorCode REQUIREMENT_HANDLER_NOT_PRODUCT_MEMBER = new ErrorCode(1_008_002_023, "当前需求负责人不是此产品团队成员,请重新选择");
ErrorCode REQUIREMENT_REVIEW_ALREADY_EXISTS = new ErrorCode(1_008_002_024, "该产品需求已提交评审记录");
ErrorCode REQUIREMENT_REVIEW_NOT_EXISTS = new ErrorCode(1_008_002_025, "产品需求评审记录不存在");
ErrorCode REQUIREMENT_REVIEW_CONCLUSION_INVALID = new ErrorCode(1_008_002_026, "产品需求评审结论不合法");
ErrorCode REQUIREMENT_NOT_PROJECT_MEMBER = new ErrorCode(1_008_002_022, "您不是该项目的成员,无权访问");
// ========== 项目管理 1-008-002-000 ==========
ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1_008_002_000, "项目不存在");
ErrorCode PROJECT_CODE_DUPLICATE = new ErrorCode(1_008_002_001, "已经存在编码为【{}】的项目");
ErrorCode PROJECT_NAME_DUPLICATE = new ErrorCode(1_008_002_002, "当前产品下已经存在名称为【{}】的项目");
ErrorCode PROJECT_CODE_NOT_MODIFIABLE = new ErrorCode(1_008_002_003, "项目编码创建后不允许修改");
ErrorCode PROJECT_PRODUCT_NOT_MODIFIABLE = new ErrorCode(1_008_002_004, "项目所属产品第一期不允许直接修改");
ErrorCode PROJECT_PRODUCT_NOT_EXISTS = new ErrorCode(1_008_002_005, "所属产品不存在或不可用");
ErrorCode PROJECT_TYPE_INVALID = new ErrorCode(1_008_002_006, "项目类型不是有效字典值");
ErrorCode PROJECT_MANAGER_USER_INVALID = new ErrorCode(1_008_002_007, "项目负责人不是有效系统用户");
ErrorCode PROJECT_MEMBER_USER_INVALID = new ErrorCode(1_008_002_008, "项目成员不是有效系统用户");
ErrorCode PROJECT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_002_009, "当前项目状态不支持动作【{}】");
ErrorCode PROJECT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_002_010, "动作【{}】必须填写原因");
ErrorCode PROJECT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_002_011, "项目状态已发生变化,请刷新后重试");
ErrorCode PROJECT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_002_012, "当前项目状态不允许编辑");
ErrorCode PROJECT_MEMBER_NOT_EXISTS = new ErrorCode(1_008_002_013, "项目成员不存在");
ErrorCode PROJECT_MEMBER_ALREADY_EXISTS = new ErrorCode(1_008_002_014, "该用户已是当前项目的有效成员");
ErrorCode PROJECT_MEMBER_ROLE_INVALID = new ErrorCode(1_008_002_015, "角色不存在或不属于项目对象角色");
ErrorCode PROJECT_MANAGER_MEMBER_NOT_ALLOW_REMOVE = new ErrorCode(1_008_002_016, "当前项目负责人不能移出项目团队,请先完成负责人变更");
ErrorCode PROJECT_MEMBER_NOT_ACTIVE = new ErrorCode(1_008_002_017, "当前项目成员已失效");
ErrorCode PROJECT_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_002_018, "删除确认口令不正确");
ErrorCode PROJECT_DELETE_NAME_MISMATCH = new ErrorCode(1_008_002_019, "删除确认名称与当前项目名称不一致");
ErrorCode PROJECT_NOT_ALLOW_DELETE = new ErrorCode(1_008_002_020, "当前项目不允许删除");
ErrorCode PROJECT_OBJECT_PERMISSION_DENIED = new ErrorCode(1_008_002_021, "当前用户不具备该项目的操作权限【{}】");
ErrorCode PROJECT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_002_022, "项目状态定义不存在或已停用");
ErrorCode PROJECT_DIRECTION_INVALID = new ErrorCode(1_008_002_023, "项目方向不是有效字典值");
ErrorCode PROJECT_MANAGER_TRANSFER_INFO_REQUIRED = new ErrorCode(1_008_002_024, "切换项目经理时必须同时传入原项目经理用户和交接后角色");
ErrorCode PROJECT_MANAGER_TRANSFER_SOURCE_INVALID = new ErrorCode(1_008_002_025, "原项目经理信息与当前项目经理不一致");
ErrorCode PROJECT_MANAGER_TRANSFER_ROLE_INVALID = new ErrorCode(1_008_002_026, "原项目经理交接后的角色不能仍为项目经理");
ErrorCode PROJECT_MANAGER_MEMBER_NOT_ALLOW_DOWNGRADE = new ErrorCode(1_008_002_027, "当前项目经理不能直接调整为非经理角色,请先完成经理转交");
ErrorCode PROJECT_MAINLINE_DUPLICATE = new ErrorCode(1_008_002_028, "当前产品下已存在未作废的主线项目");
// 项目创建原子接口(/create-with-team专用初始团队 + 方向一致性校验
ErrorCode PROJECT_INITIAL_TEAM_MANAGER_REQUIRED = new ErrorCode(1_008_002_029, "初始团队必须包含项目经理");
ErrorCode PROJECT_INITIAL_TEAM_MEMBER_DUPLICATE = new ErrorCode(1_008_002_030, "初始团队成员存在重复");
ErrorCode PROJECT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_002_031, "初始团队中存在非法角色");
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, "执行不存在");
ErrorCode PROJECT_EXECUTION_NAME_DUPLICATE = new ErrorCode(1_008_003_001, "当前项目下已经存在名称为【{}】的执行");
ErrorCode PROJECT_EXECUTION_OWNER_INVALID = new ErrorCode(1_008_003_002, "执行负责人必须是当前项目的有效成员");
ErrorCode PROJECT_EXECUTION_ASSIGNEE_INVALID = new ErrorCode(1_008_003_003, "执行协办人必须是当前项目的有效成员");
ErrorCode PROJECT_EXECUTION_ASSIGNEE_ALREADY_EXISTS = new ErrorCode(1_008_003_004, "该用户已是当前执行的有效协办人");
ErrorCode PROJECT_EXECUTION_ASSIGNEE_NOT_EXISTS = new ErrorCode(1_008_003_005, "执行协办人不存在");
ErrorCode PROJECT_EXECUTION_ASSIGNEE_NOT_ACTIVE = new ErrorCode(1_008_003_006, "当前执行协办人已失效");
// 保留TD-013 解锁后业务路径已不会再触发,预留用于灰度回滚关闭关联能力
ErrorCode PROJECT_EXECUTION_REQUIREMENT_NOT_READY = new ErrorCode(1_008_003_007, "当前阶段不支持给执行绑定项目需求");
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_008, "当前项目状态不允许维护执行");
ErrorCode PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED = new ErrorCode(1_008_003_009, "该项目成员仍担任未终态执行负责人,请先完成执行负责人交接");
ErrorCode PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_003_010, "执行状态定义不存在或已停用");
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_003_011, "当前执行状态不支持动作【{}】");
ErrorCode PROJECT_EXECUTION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_003_012, "动作【{}】必须填写原因");
ErrorCode PROJECT_EXECUTION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_003_013, "执行状态已发生变化,请刷新后重试");
ErrorCode PROJECT_EXECUTION_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_003_014, "当前执行状态不允许维护执行");
ErrorCode PROJECT_EXECUTION_TYPE_INVALID = new ErrorCode(1_008_003_015, "执行类型不是有效字典值");
ErrorCode PROJECT_EXECUTION_ASSIGNEE_REQUIRED = new ErrorCode(1_008_003_016, "创建执行时必须至少选择一名执行协办人");
ErrorCode PROJECT_EXECUTION_STATUS_OWNER_ONLY = new ErrorCode(1_008_003_017, "只有执行负责人才能执行【{}】动作");
ErrorCode PROJECT_EXECUTION_COMPLETE_TASKS_REQUIRED = new ErrorCode(1_008_003_018, "完成执行前,执行下所有任务必须全部完成或取消");
ErrorCode PROJECT_EXECUTION_NOT_ALLOW_DELETE = new ErrorCode(1_008_003_019, "已完成的执行不允许删除");
ErrorCode PROJECT_EXECUTION_DELETE_NAME_MISMATCH = new ErrorCode(1_008_003_020, "确认执行名称与实际不一致");
ErrorCode PROJECT_EXECUTION_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_003_021, "删除确认口令必须为 DELETE 或 删除");
ErrorCode PROJECT_EXECUTION_PRIORITY_INVALID = new ErrorCode(1_008_003_022, "执行优先级不是有效字典值");
ErrorCode PROJECT_EXECUTION_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_003_023, "关联的项目需求不存在或已删除");
ErrorCode PROJECT_EXECUTION_REQUIREMENT_NOT_BELONG_TO_PROJECT = new ErrorCode(1_008_003_024, "关联的项目需求不属于当前项目");
ErrorCode PROJECT_EXECUTION_REQUIREMENT_TERMINAL = new ErrorCode(1_008_003_025, "项目需求已处于终态,不允许关联新执行");
// ========== 任务管理 1-008-004-000 ==========
ErrorCode PROJECT_TASK_NOT_EXISTS = new ErrorCode(1_008_004_000, "任务不存在");
ErrorCode PROJECT_TASK_OWNER_INVALID = new ErrorCode(1_008_004_001, "任务负责人必须是当前执行的有效成员");
ErrorCode PROJECT_TASK_PARENT_INVALID = new ErrorCode(1_008_004_002, "父任务必须属于当前项目和执行");
ErrorCode PROJECT_TASK_NOT_ALLOW_EDIT = new ErrorCode(1_008_004_003, "当前项目或执行状态不允许维护任务");
ErrorCode PROJECT_TASK_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_004_004, "任务状态定义不存在或已停用");
ErrorCode PROJECT_TASK_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_004_005, "当前任务状态不支持动作【{}】");
ErrorCode PROJECT_TASK_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_004_006, "动作【{}】必须填写原因");
ErrorCode PROJECT_TASK_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_004_007, "任务状态已发生变化,请刷新后重试");
ErrorCode PROJECT_TASK_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_004_008, "当前任务状态不允许维护任务");
ErrorCode PROJECT_TASK_COMPLETE_CHILDREN_REQUIRED = new ErrorCode(1_008_004_010, "父任务完成前,子任务必须全部完成或取消");
ErrorCode PROJECT_TASK_STATUS_OWNER_ONLY = new ErrorCode(1_008_004_011, "只有任务负责人才能执行【{}】动作");
ErrorCode PROJECT_TASK_NOT_ALLOW_DELETE = new ErrorCode(1_008_004_012, "已完成的任务不允许删除");
ErrorCode PROJECT_TASK_DELETE_NAME_MISMATCH = new ErrorCode(1_008_004_013, "确认任务名称与实际不一致");
ErrorCode PROJECT_TASK_DELETE_CONFIRM_TEXT_INVALID = new ErrorCode(1_008_004_014, "删除确认口令必须为 DELETE 或 删除");
ErrorCode PROJECT_TASK_PRIORITY_INVALID = new ErrorCode(1_008_004_015, "任务优先级不是有效字典值");
ErrorCode PROJECT_TASK_LEAF_TO_PARENT_FORBIDDEN_PROGRESS = new ErrorCode(1_008_004_012, "拆子任务前请先将父任务进度清零");
ErrorCode PROJECT_TASK_LEAF_TO_PARENT_FORBIDDEN_WORKLOG = new ErrorCode(1_008_004_013, "拆子任务前请先删除父任务下已填的工时记录");
// ========== 任务协办人 1_008_005_xxx ==========
ErrorCode PROJECT_TASK_ASSIGNEE_NOT_EXISTS = new ErrorCode(1_008_005_001, "任务协办人记录不存在");
ErrorCode PROJECT_TASK_ASSIGNEE_INVALID_MEMBER = new ErrorCode(1_008_005_002, "任务协办人必须是当前有效的执行成员");
ErrorCode PROJECT_TASK_ASSIGNEE_OWNER_CONFLICT = new ErrorCode(1_008_005_003, "任务协办人不能与任务负责人重复");
ErrorCode PROJECT_TASK_ASSIGNEE_ALREADY_ACTIVE = new ErrorCode(1_008_005_004, "该用户已是当前任务的活跃协办人");
ErrorCode PROJECT_TASK_ASSIGNEE_NOT_ACTIVE = new ErrorCode(1_008_005_005, "任务协办人记录已失效,无需重复操作");
ErrorCode PROJECT_TASK_ASSIGNEE_REASON_REQUIRED = new ErrorCode(1_008_005_006, "退出协办必须填写原因");
// ========== 任务工时 1_008_006_xxx ==========
ErrorCode PROJECT_TASK_WORKLOG_NOT_EXISTS = new ErrorCode(1_008_006_001, "任务工时记录不存在");
ErrorCode PROJECT_TASK_WORKLOG_NOT_LEAF_TASK = new ErrorCode(1_008_006_002, "父任务不允许填报工时,请到具体子任务填报");
ErrorCode PROJECT_TASK_WORKLOG_DURATION_INVALID = new ErrorCode(1_008_006_003, "工时小时数必须大于 0 且为 0.5 的整数倍");
ErrorCode PROJECT_TASK_WORKLOG_NOT_OWNER_OR_ASSIGNEE = new ErrorCode(1_008_006_004, "仅任务负责人或在岗协办人可填报工时");
ErrorCode PROJECT_TASK_WORKLOG_EDIT_NOT_OWN = new ErrorCode(1_008_006_005, "只能修改自己填报的工时记录");
ErrorCode PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN = new ErrorCode(1_008_006_006, "仅记录填报人或任务负责人可删除该工时记录");
ErrorCode PROJECT_TASK_WORKLOG_DATE_RANGE_INVALID = new ErrorCode(1_008_006_007, "段起始日期不能晚于段结束日期");
ErrorCode PROJECT_TASK_WORKLOG_DATE_OVERLAP = new ErrorCode(1_008_006_008, "日期范围与该任务下您已有的工时记录重叠");
ErrorCode PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC = new ErrorCode(1_008_006_010, "工时进度与日期顺序不一致:早段进度不得高于晚段、晚段进度不得低于早段");
ErrorCode PROJECT_TASK_WORKLOG_DIFFICULTY_INVALID = new ErrorCode(1_008_006_011, "完成难度不在字典范围内");
// ========== 任务 / 工时附件 1_008_007_xxx ==========
ErrorCode PROJECT_TASK_ATTACHMENT_TOO_MANY = new ErrorCode(1_008_007_001, "附件数量不能超过 {} 个");
ErrorCode PROJECT_TASK_ATTACHMENT_URL_INVALID = new ErrorCode(1_008_007_002, "附件地址非法,必须为 http/https URL 且长度不超过 1024");
ErrorCode PROJECT_TASK_ATTACHMENT_NAME_INVALID = new ErrorCode(1_008_007_003, "附件文件名不合法(必填且长度不超过 255");
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_NOT_ALLOWED = new ErrorCode(1_008_007_004, "附件扩展名【{}】不在允许列表内");
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_BLOCKED = new ErrorCode(1_008_007_005, "附件类型【{}】被禁止上传");
// ========== 项目需求 1_008_007_xxx ==========
ErrorCode PROJECT_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "项目需求不存在");
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_007_001, "当前项目需求状态不支持动作【{}】");
ErrorCode PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_007_002, "动作【{}】必须填写原因");
ErrorCode PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_007_003, "项目需求状态已发生变化,请刷新后重试");
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_007_004, "当前项目需求状态不允许编辑");
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_005, "只有已验收的项目需求才能关闭");
ErrorCode PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_007_006, "项目需求状态定义不存在或已停用");
ErrorCode PROJECT_REQUIREMENT_PARENT_NOT_ALLOW_SPLIT = new ErrorCode(1_008_007_007, "父需求状态不是已评审、实施中,不允许拆分");
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CLOSE = new ErrorCode(1_008_007_008, "存在未处理完的子需求,请先处理子需求");
ErrorCode PROJECT_REQUIREMENT_MODULE_NOT_EXISTS = new ErrorCode(1_008_007_009, "项目需求模块不存在");
ErrorCode PROJECT_REQUIREMENT_MODULE_NAME_DUPLICATE = new ErrorCode(1_008_007_010, "已经存在名称为【{}】的项目需求模块");
ErrorCode PROJECT_REQUIREMENT_MODULE_NOT_BELONG_TO_PROJECT = new ErrorCode(1_008_007_011, "模块不属于当前项目");
ErrorCode PROJECT_REQUIREMENT_HAS_CHILDREN = new ErrorCode(1_008_007_013, "存在子需求,请先删除子需求");
ErrorCode PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_007_014, "只有待认领、待评审状态的项目需求才能删除");
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_CHILDREN = new ErrorCode(1_008_007_015, "存在子模块,请先删除子模块");
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_007_016, "模块下存在项目需求,请先删除需求");
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_017, "只有不存在子需求,或子需求都处于已取消和已拒绝状态时,父需求才允许取消");
ErrorCode PROJECT_REQUIREMENT_HAS_EXECUTIONS_NOT_ALLOW_DELETE = new ErrorCode(1_008_007_018, "该项目需求下存在承接执行,请先解绑或转移");
ErrorCode PROJECT_REQUIREMENT_SYNCED_FROM_PRODUCT_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_019, "从产品侧流转来的需求不可取消");
ErrorCode PROJECT_REQUIREMENT_REVIEW_ALREADY_EXISTS = new ErrorCode(1_008_007_020, "该项目需求已提交评审记录");
ErrorCode PROJECT_REQUIREMENT_REVIEW_NOT_EXISTS = new ErrorCode(1_008_007_021, "项目需求评审记录不存在");
ErrorCode PROJECT_REQUIREMENT_REVIEW_CONCLUSION_INVALID = new ErrorCode(1_008_007_022, "项目需求评审结论不合法");
// ========== 个人事项 1_008_008_xxx ==========
ErrorCode PERSONAL_ITEM_NOT_EXISTS = new ErrorCode(1_008_008_001, "个人事项不存在");
ErrorCode PERSONAL_ITEM_OWNER_NOT_IN_EXECUTION = new ErrorCode(1_008_008_002, "个人事项负责人必须属于当前有效执行团队成员");
ErrorCode PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_008_003, "个人事项状态定义不存在或已停用");
ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_008_004, "当前个人事项状态不支持动作【{}】");
ErrorCode PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_008_005, "动作【{}】必须填写原因");
ErrorCode PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_008_006, "个人事项状态已发生变化,请刷新后重试");
ErrorCode PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_008_007, "当前个人事项状态不允许编辑");
ErrorCode PERSONAL_ITEM_NOT_ALLOW_DELETE = new ErrorCode(1_008_008_008, "仅初始态(待开始)的个人事项允许删除");
ErrorCode PERSONAL_ITEM_WRITE_FORBIDDEN = new ErrorCode(1_008_008_009, "无权修改个人事项");
// ========== 加班申请 1_008_009_xxx ==========
ErrorCode OVERTIME_APPLICATION_NOT_EXISTS = new ErrorCode(1_008_009_001, "加班申请不存在");
ErrorCode OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_009_002, "加班申请状态定义不存在或已停用");
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_009_003, "当前加班申请状态不支持动作【{}】");
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_009_004, "动作【{}】必须填写原因");
ErrorCode OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_009_005, "加班申请状态已发生变化,请刷新后重试");
ErrorCode OVERTIME_APPLICATION_APPLICANT_ONLY = new ErrorCode(1_008_009_006, "仅申请人可执行该操作");
ErrorCode OVERTIME_APPLICATION_APPROVER_ONLY = new ErrorCode(1_008_009_007, "仅当前审核人可执行该操作");
ErrorCode OVERTIME_APPLICATION_APPROVER_INVALID = new ErrorCode(1_008_009_008, "审核人不是有效系统用户");
ErrorCode OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN = new ErrorCode(1_008_009_009, "审核人不能选择申请人本人");
ErrorCode OVERTIME_APPLICATION_READ_FORBIDDEN = new ErrorCode(1_008_009_010, "无权查看该加班申请");
ErrorCode OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED = new ErrorCode(1_008_009_011, "仅已撤销的加班申请允许删除");
} }

View File

@@ -1,13 +1,28 @@
package com.njcn.rdms.module.project.enums; package com.njcn.rdms.module.project.enums;
/** /**
* 项目交付域字典类型常量 * 项目域字典类型常量
*/ */
public interface ProjectDictTypeConstants { public interface ProjectDictTypeConstants {
/** /**
* 产品方向 * 项目类型。
*/ */
String PRODUCT_DIRECTION = "rdms_product_direction"; String PROJECT_TYPE = "rdms_project_type";
/**
* 执行类型。
*/
String EXECUTION_TYPE = "rdms_project_execution_type";
/**
* 优先级(任务 / 执行 共用P0=最高 ~ P3=最低)。
*/
String REQ_PRIORITY = "rdms_req_priority";
/**
* 工时完成难度。
*/
String WORKLOG_DIFFICULTY = "rdms_worklog_difficulty";
} }

View File

@@ -1,49 +0,0 @@
# 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 项:主表补终态结果字段,审计日志继续完整留痕

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,168 @@
package com.njcn.rdms.module.project.constant;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Set;
/**
* 对象动态常量
*
* 说明:
* 1. 当前先承载产品对象首页时间线使用的 activityType / actionType 常量
* 2. 后续项目等对象复用同类动态能力时,继续按前缀分组扩展,不单独拆分枚举
*/
public final class ObjectActivityConstants {
private ObjectActivityConstants() {
}
// ========== 动态来源类型 ==========
public static final String ACTIVITY_TYPE_STATUS = "status";
public static final String ACTIVITY_TYPE_PRODUCT = "product";
public static final String ACTIVITY_TYPE_MEMBER = "member";
// ========== 审计业务类型 ==========
public static final String PRODUCT_BIZ_TYPE = "product";
public static final String PROJECT_BIZ_TYPE = "project";
public static final String MEMBER_BIZ_TYPE = "rdms_user_object_role";
// ========== 产品对象动作 ==========
public static final String PRODUCT_ACTION_CREATE = "create";
public static final String PRODUCT_ACTION_UPDATE = "update";
public static final String PRODUCT_ACTION_DELETE = "delete";
public static final String PRODUCT_ACTION_CHANGE_MANAGER = "change_manager";
// ========== 项目对象动作 ==========
public static final String PROJECT_ACTION_CREATE = "create";
public static final String PROJECT_ACTION_UPDATE = "update";
public static final String PROJECT_ACTION_DELETE = "delete";
public static final String PROJECT_ACTION_CHANGE_MANAGER = "change_manager";
public static final String PROJECT_ACTION_AUTO_START = "auto_start";
// ========== 项目自动推进触发动作 ==========
public static final String PROJECT_TRIGGER_CREATE_EXECUTION = "create_execution";
public static final String PROJECT_TRIGGER_CREATE_TASK = "create_task";
public static final String PROJECT_TRIGGER_SCHEDULE_REQUIREMENT = "schedule_requirement";
/**
* 项目自动开始触发动作 —— 由执行 auto_start 反向触发。
* 与 {@link #EXECUTION_TRIGGER_AUTO_START} 同语义;项目侧用此名走 AUTO_START_TRIGGERS 白名单。
*/
public static final String PROJECT_TRIGGER_EXECUTION_AUTO_START = "execution_auto_start";
// ========== 任务自动推进触发动作 ==========
/**
* 任务自动开始动作编码 —— owner 或协办人填报工时时由后端自动触发的状态流转动作。
* 对应 rdms_object_status_transition (object_type=task, action_code=auto_start,
* from=pending → to=active)。
* 修改 DB 该条记录的 action_code 时必须同步修改本常量;本常量被
* ProjectTaskServiceImpl#internalAutoStartByWorklog 引用。
* 后续若再增加"任务自动完成 / 执行自动开始"等同类语义,按 *_ACTION_AUTO_* 命名加在本分组。
*/
public static final String TASK_ACTION_AUTO_START = "auto_start";
// ========== 状态动作 ==========
public static final String STATUS_ACTION_PAUSE = "pause";
public static final String STATUS_ACTION_RESUME = "resume";
public static final String STATUS_ACTION_ARCHIVE = "archive";
public static final String STATUS_ACTION_ABANDON = "abandon";
// ========== 成员动作 ==========
public static final String MEMBER_ACTION_ADD = "add_member";
public static final String MEMBER_ACTION_UPDATE = "update_member";
public static final String MEMBER_ACTION_REMOVE = "remove_member";
/**
* 复活动作:原 INACTIVE 成员行被重新激活status: 1 → 0用于把"再次新增 / update 改 role 命中老 INACTIVE 行"路径
* 跟物理"新增 / 更新"的 audit 语义区分开。createXxxMember 命中 INACTIVE 三元组复活老行时使用本动作,避免 ADD 语义误用。
*/
public static final String MEMBER_ACTION_REACTIVATE = "reactivate_member";
public static final String EXECUTION_ACTION_CREATE = "create_execution_entity";
public static final String EXECUTION_ACTION_UPDATE = "update_execution_entity";
public static final String EXECUTION_ACTION_DELETE = "delete_execution_entity";
public static final String EXECUTION_ACTION_CHANGE_OWNER = "change_execution_owner";
public static final String EXECUTION_ASSIGNEE_ACTION_ADD = "add_execution_assignee";
public static final String EXECUTION_ASSIGNEE_ACTION_REMOVE = "remove_execution_assignee";
public static final String TASK_ACTION_CREATE = "create_task_entity";
public static final String TASK_ACTION_UPDATE = "update_task_entity";
public static final String TASK_ACTION_DELETE = "delete_task_entity";
public static final String PERSONAL_ITEM_ACTION_CREATE = "create_personal_item";
public static final String PERSONAL_ITEM_ACTION_UPDATE = "update_personal_item";
public static final String PERSONAL_ITEM_ACTION_DELETE = "delete_personal_item";
// ========== 任务协办人事件类型B 模型 - 多行周期记录) ==========
public static final String TASK_ASSIGNEE_ACTION_JOIN = "join";
public static final String TASK_ASSIGNEE_ACTION_INACTIVE = "inactive";
// ========== 执行协办人事件类型B 模型 - 多行周期记录) ==========
public static final String EXECUTION_ASSIGNEE_LOG_ACTION_JOIN = "join";
public static final String EXECUTION_ASSIGNEE_LOG_ACTION_INACTIVE = "inactive";
public static final String EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN = "owner_transfer_in";
public static final String EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_OUT = "owner_transfer_out";
public static final List<String> STATUS_ACTION_TYPES = List.of(
STATUS_ACTION_PAUSE, STATUS_ACTION_RESUME, STATUS_ACTION_ARCHIVE, STATUS_ACTION_ABANDON);
public static final List<String> PRODUCT_TIMELINE_ACTION_TYPES = List.of(
PRODUCT_ACTION_CREATE, PRODUCT_ACTION_CHANGE_MANAGER);
public static final List<String> MEMBER_TIMELINE_ACTION_TYPES = List.of(
MEMBER_ACTION_ADD, MEMBER_ACTION_UPDATE, MEMBER_ACTION_REMOVE, MEMBER_ACTION_REACTIVATE);
private static final Set<String> STATUS_ACTION_TYPE_SET = Set.copyOf(STATUS_ACTION_TYPES);
public static boolean isStatusAction(String actionType) {
return STATUS_ACTION_TYPE_SET.contains(normalize(actionType));
}
public static String resolveActionName(String actionType) {
String normalizedActionType = normalize(actionType);
if (!StringUtils.hasText(normalizedActionType)) {
return actionType;
}
return switch (normalizedActionType) {
case PRODUCT_ACTION_CREATE -> "创建";
case PRODUCT_ACTION_UPDATE -> "更新";
case PRODUCT_ACTION_DELETE -> "删除";
case PROJECT_ACTION_AUTO_START -> "开始推进";
case PROJECT_TRIGGER_CREATE_EXECUTION -> "创建执行";
case PROJECT_TRIGGER_CREATE_TASK -> "创建任务";
case PROJECT_TRIGGER_SCHEDULE_REQUIREMENT -> "项目需求排期";
case PROJECT_TRIGGER_EXECUTION_AUTO_START -> "执行自动开始";
case EXECUTION_ACTION_CREATE -> "创建执行";
case EXECUTION_ACTION_UPDATE -> "更新执行";
case EXECUTION_ACTION_DELETE -> "删除执行";
case EXECUTION_ACTION_CHANGE_OWNER -> "变更执行负责人";
case EXECUTION_ASSIGNEE_ACTION_ADD -> "新增执行协办人";
case EXECUTION_ASSIGNEE_ACTION_REMOVE -> "移出执行协办人";
case TASK_ACTION_CREATE -> "创建任务";
case TASK_ACTION_UPDATE -> "更新任务";
case TASK_ACTION_DELETE -> "删除任务";
case PERSONAL_ITEM_ACTION_CREATE -> "创建个人事项";
case PERSONAL_ITEM_ACTION_UPDATE -> "更新个人事项";
case PERSONAL_ITEM_ACTION_DELETE -> "删除个人事项";
case TASK_ASSIGNEE_ACTION_JOIN -> "加入";
case TASK_ASSIGNEE_ACTION_INACTIVE -> "退出";
case EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN -> "转入负责人";
case EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_OUT -> "转出负责人";
case "start" -> "开始";
case "block" -> "阻塞";
case "complete" -> "完成";
case "cancel" -> "取消";
case STATUS_ACTION_PAUSE -> "暂停";
case STATUS_ACTION_RESUME -> "恢复";
case STATUS_ACTION_ARCHIVE -> "归档";
case STATUS_ACTION_ABANDON -> "废弃";
case PRODUCT_ACTION_CHANGE_MANAGER -> "切换负责人";
case MEMBER_ACTION_ADD -> "新增成员";
case MEMBER_ACTION_UPDATE -> "调整成员";
case MEMBER_ACTION_REMOVE -> "移出成员";
case MEMBER_ACTION_REACTIVATE -> "重新激活成员";
default -> normalizedActionType;
};
}
private static String normalize(String value) {
return StringUtils.hasText(value) ? value.trim() : null;
}
}

View File

@@ -0,0 +1,28 @@
package com.njcn.rdms.module.project.constant;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
/**
* RDMS 对象角色与成员关系常量。
*/
public final class ObjectRoleConstants {
private ObjectRoleConstants() {
}
/**
* 对象级权限作用域,对应 sys_menu.scope_type = object。
*/
public static final String ROLE_SCOPE_OBJECT = PermissionScopeTypeEnum.OBJECT.getScopeType();
/**
* 对象成员有效状态,对应 rdms_user_object_role.status = 0。
*/
public static final Integer MEMBER_STATUS_ACTIVE = 0;
/**
* 对象成员失效状态,对应 rdms_user_object_role.status = 1。
*/
public static final Integer MEMBER_STATUS_INACTIVE = 1;
}

View File

@@ -0,0 +1,38 @@
package com.njcn.rdms.module.project.constant;
/**
* 加班申请常量。
*/
public final class OvertimeApplicationConstants {
private OvertimeApplicationConstants() {
}
public static final String BIZ_TYPE = "overtime_application";
public static final String STATUS_OBJECT_TYPE = BIZ_TYPE;
public static final String STATUS_PENDING = "pending";
public static final String STATUS_APPROVED = "approved";
public static final String STATUS_REJECTED = "rejected";
public static final String STATUS_CANCELLED = "cancelled";
/**
* 新建即提交的业务日志动作。
* <p>
* 该动作仅用于状态日志、审计日志留痕,不要求在 rdms_object_status_transition 中存在显式流转配置。
*/
public static final String ACTION_SUBMIT = "submit";
public static final String ACTION_RESUBMIT = "resubmit";
public static final String ACTION_APPROVE = "approve";
public static final String ACTION_REJECT = "reject";
public static final String ACTION_CANCEL = "cancel";
public static final String ACTION_DELETE = "delete";
public static final String PERMISSION_QUERY = "project:overtime-application:query";
public static final String PERMISSION_CREATE = "project:overtime-application:create";
public static final String PERMISSION_UPDATE = "project:overtime-application:update";
public static final String PERMISSION_DELETE = "project:overtime-application:delete";
public static final String PERMISSION_APPROVE = "project:overtime-application:approve";
public static final String PERMISSION_EXPORT = "project:overtime-application:export";
}

View File

@@ -0,0 +1,28 @@
package com.njcn.rdms.module.project.constant;
/**
* 个人事项常量。
*/
public final class PersonalItemConstants {
private PersonalItemConstants() {
}
/**
* 个人事项状态模型复用任务对象类型。
*/
public static final String STATUS_OBJECT_TYPE = ProjectTaskConstants.OBJECT_TYPE;
/**
* 个人事项业务类型。
*/
public static final String BIZ_TYPE = "personal_item";
public static final String PERMISSION_QUERY = "project:personal-item:query";
public static final String PERMISSION_CREATE = "project:personal-item:create";
public static final String PERMISSION_UPDATE = "project:personal-item:update";
public static final String PERMISSION_DELETE = "project:personal-item:delete";
public static final String PERMISSION_STATUS = "project:personal-item:status";
public static final String STATUS_COMPLETED = "completed";
}

View File

@@ -0,0 +1,56 @@
package com.njcn.rdms.module.project.constant;
/**
* 产品对象常量。
*/
public final class ProductObjectConstants {
private ProductObjectConstants() {
}
/**
* 产品对象类型,对应 rdms_user_object_role.object_type、rdms_object_status_model.object_type。
*/
public static final String OBJECT_TYPE = "product";
/**
* 产品经理对象角色编码。该角色用于初始化与识别产品负责人。
*/
public static final String MANAGER_ROLE_CODE = "product_manager";
/**
* 产品暂停状态。
*/
public static final String STATUS_PAUSED = "paused";
/**
* 产品自动编码前缀。
*/
public static final String CODE_PREFIX = "CNPD";
/**
* 产品编辑权限码。
*/
public static final String PERMISSION_UPDATE = "project:product:update";
/**
* 产品状态动作权限码。
*/
public static final String PERMISSION_STATUS = "project:product:status";
/**
* 产品需求评审权限码。
*/
public static final String PERMISSION_REVIEW = "project:product:review";
/**
* 产品删除权限码。
*/
public static final String PERMISSION_DELETE = "project:product:delete";
/**
* 产品删除确认口令。
*/
public static final String DELETE_CONFIRM_TEXT = "DELETE";
}

View File

@@ -0,0 +1,73 @@
package com.njcn.rdms.module.project.constant;
import java.util.Set;
/**
* 执行对象常量。
*/
public final class ProjectExecutionConstants {
private ProjectExecutionConstants() {
}
/**
* 执行对象类型,对应 rdms_object_status_model.object_type。
*/
public static final String OBJECT_TYPE = "execution";
/**
* 执行业务类型。
*/
public static final String BIZ_TYPE = "project_execution";
/**
* 执行读路径查询权限码对象域object_type='project')。
* 覆盖执行对象所有读路径page / status-board / detail。
* "我参与 / 所有"视角由前端发不发 involveUserId 决定;进得来 = 看项目下全部,无此权限码直接 403。
*/
public static final String PERMISSION_QUERY = "project:execution:query";
/**
* 创建执行权限码。
*/
public static final String PERMISSION_CREATE = "project:execution:create";
/**
* 编辑执行权限码。
*/
public static final String PERMISSION_UPDATE = "project:execution:update";
/**
* 执行负责人治理权限码。
*/
public static final String PERMISSION_OWNER = "project:execution:owner";
/**
* 执行协办人治理权限码。
*/
public static final String PERMISSION_ASSIGNEE = "project:execution:assignee";
/**
* 执行状态动作权限码。
*/
public static final String PERMISSION_STATUS = "project:execution:status";
/**
* 删除执行权限码。
* 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。
*/
public static final String PERMISSION_DELETE = "project:execution:delete";
/**
* 执行"已完成"状态码,对应 rdms_object_status_model 中 object_type='execution' 且 status_code='completed' 的状态。
* 删除时拒绝主动删除(已完成的执行不允许删除)。
*/
public static final String STATUS_COMPLETED = "completed";
/**
* 删除确认口令合法值集合;兼容大写英文 "DELETE" 与中文 "删除",前端可纯中文文案。
* 校验时精确匹配trim 后比对)。
*/
public static final Set<String> DELETE_CONFIRM_TEXTS = Set.of("DELETE", "删除");
}

View File

@@ -0,0 +1,87 @@
package com.njcn.rdms.module.project.constant;
import java.util.Set;
/**
* 项目对象常量。
*/
public final class ProjectObjectConstants {
private ProjectObjectConstants() {
}
/**
* 项目对象类型,对应 rdms_user_object_role.object_type、rdms_object_status_model.object_type。
*/
public static final String OBJECT_TYPE = "project";
/**
* 项目经理对象角色编码。该角色用于初始化与识别项目负责人。
*/
public static final String MANAGER_ROLE_CODE = "project_manager";
/**
* 项目游客对象角色编码。创建人未成为项目成员时,用于返回只读上下文菜单。
*/
public static final String VISITOR_ROLE_CODE = "visitor";
/**
* 产品主线项目类型字典值。后续字典值收敛后,只需调整这里。
*/
public static final Set<String> MAINLINE_PROJECT_TYPE_CODES = Set.of("mainline", "main", "product_mainline", "主线");
/**
* 已作废项目状态编码。当前作废项目不占用产品主线项目名额。
*/
public static final String STATUS_CANCELLED = "cancelled";
/**
* 项目自动编码前缀。
*/
public static final String CODE_PREFIX = "CNPJ";
/**
* 项目编辑权限码。
*/
public static final String PERMISSION_UPDATE = "project:project:update";
/**
* 项目成员维护权限码。
*/
public static final String PERMISSION_MEMBER = "project:project:member";
/**
* 项目状态动作权限码。
*/
public static final String PERMISSION_STATUS = "project:project:status";
/**
* 项目需求评审权限码。
*/
public static final String PERMISSION_REVIEW = "project:project:review";
/**
* 项目拆分权限码。
*/
public static final String PERMISSION_SPLIT = "project:project:split";
/**
* 项目删除权限码。
*/
public static final String PERMISSION_DELETE = "project:project:delete";
/**
* 项目删除确认口令。
*/
public static final String DELETE_CONFIRM_TEXT = "DELETE";
/**
* 可触发项目从待开始自动进入进行中的后端业务动作。
*/
public static final Set<String> AUTO_START_TRIGGERS = Set.of(
ObjectActivityConstants.PROJECT_TRIGGER_CREATE_EXECUTION,
ObjectActivityConstants.PROJECT_TRIGGER_CREATE_TASK,
ObjectActivityConstants.PROJECT_TRIGGER_SCHEDULE_REQUIREMENT,
ObjectActivityConstants.PROJECT_TRIGGER_EXECUTION_AUTO_START);
}

View File

@@ -0,0 +1,75 @@
package com.njcn.rdms.module.project.constant;
import java.util.Set;
/**
* 任务对象常量。
*/
public final class ProjectTaskConstants {
private ProjectTaskConstants() {
}
/**
* 任务对象类型,对应 rdms_object_status_model.object_type。
*/
public static final String OBJECT_TYPE = "task";
/**
* 任务"已完成"状态码,对应 rdms_object_status_model 中 object_type='task' 且 status_code='completed' 的状态。
* 用于 execution 的 complete 按钮可见性判定:要求根任务在排除排除集后全部为该状态。
*/
public static final String STATUS_COMPLETED = "completed";
/**
* 任务业务类型。
*/
public static final String BIZ_TYPE = "project_task";
/**
* 任务读路径查询权限码对象域object_type='project')。
* 覆盖任务对象所有读路径page / status-board / board-page / detail / summary含跨执行 aggregate
* "我参与 / 所有"视角由前端发不发 involveUserId 决定;进得来 = 看项目下全部,无此权限码直接 403。
*/
public static final String PERMISSION_QUERY = "project:task:query";
/**
* 创建任务权限码。
*/
public static final String PERMISSION_CREATE = "project:task:create";
/**
* 编辑任务权限码。
*/
public static final String PERMISSION_UPDATE = "project:task:update";
/**
* 任务状态动作权限码。
*/
public static final String PERMISSION_STATUS = "project:task:status";
/**
* 任务协办人维护权限码。
*/
public static final String PERMISSION_ASSIGNEE = "project:task:assignee";
/**
* 任务工时维护权限码。
*/
public static final String PERMISSION_WORKLOG = "project:task:worklog";
/**
* 删除任务权限码。
* 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。
* 实际拦截service 内按 task.parentTaskId 分流走 checkOwnerOrProjectPermission
* (一级任务 → execution.ownerId 字段身份;子任务 → parentTask.ownerId 字段身份;不命中字段时回落本权限码)。
*/
public static final String PERMISSION_DELETE = "project:task:delete";
/**
* 删除确认口令合法值集合;兼容大写英文 "DELETE" 与中文 "删除",前端可纯中文文案。
* 校验时精确匹配trim 后比对)。
*/
public static final Set<String> DELETE_CONFIRM_TEXTS = Set.of("DELETE", "删除");
}

View File

@@ -0,0 +1,135 @@
package com.njcn.rdms.module.project.controller.admin.overtime;
import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
import com.njcn.rdms.module.project.service.overtime.OvertimeApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 加班申请")
@RestController
@RequestMapping("/project/overtime-applications")
@Validated
public class OvertimeApplicationController {
@Resource
private OvertimeApplicationService overtimeApplicationService;
@PostMapping
@Operation(summary = "新增加班申请并提交")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_CREATE + "')")
public CommonResult<Long> create(@Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) {
return success(overtimeApplicationService.createApplication(reqVO));
}
@PutMapping("/{id}")
@Operation(summary = "退回或撤销后修改并重新提交加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> resubmit(@PathVariable("id") Long id,
@Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) {
overtimeApplicationService.resubmitApplication(id, reqVO);
return success(true);
}
@GetMapping("/{id}")
@Operation(summary = "获取加班申请详情")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
public CommonResult<OvertimeApplicationRespVO> get(@PathVariable("id") Long id) {
return success(overtimeApplicationService.getApplication(id));
}
@GetMapping("/page")
@Operation(summary = "获取我的加班申请分页")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
public CommonResult<PageResult<OvertimeApplicationRespVO>> page(@Valid OvertimeApplicationPageReqVO reqVO) {
return success(overtimeApplicationService.getMyPage(reqVO));
}
@GetMapping("/approval-page")
@Operation(summary = "获取待我审批的加班申请分页")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
public CommonResult<PageResult<OvertimeApplicationRespVO>> approvalPage(@Valid OvertimeApplicationPageReqVO reqVO) {
return success(overtimeApplicationService.getApprovalPage(reqVO));
}
@PostMapping("/{id}/approve")
@Operation(summary = "审核通过加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
public CommonResult<Boolean> approve(@PathVariable("id") Long id,
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
overtimeApplicationService.approve(id, reqVO);
return success(true);
}
@PostMapping("/{id}/reject")
@Operation(summary = "审核退回加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
public CommonResult<Boolean> reject(@PathVariable("id") Long id,
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
overtimeApplicationService.reject(id, reqVO);
return success(true);
}
@PostMapping("/{id}/cancel")
@Operation(summary = "撤销加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> cancel(@PathVariable("id") Long id,
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
overtimeApplicationService.cancel(id, reqVO);
return success(true);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除已撤销的加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_DELETE + "')")
public CommonResult<Boolean> delete(@PathVariable("id") Long id) {
overtimeApplicationService.deleteApplication(id);
return success(true);
}
@GetMapping("/{id}/status-logs")
@Operation(summary = "获取加班申请状态日志")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
public CommonResult<List<OvertimeApplicationStatusLogRespVO>> statusLogs(@PathVariable("id") Long id) {
return success(overtimeApplicationService.getStatusLogs(id));
}
@GetMapping("/export")
@Operation(summary = "导出我的加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_EXPORT + "')")
@ApiAccessLog(operateType = EXPORT)
public void export(@Valid OvertimeApplicationPageReqVO reqVO, HttpServletResponse response) throws IOException {
List<OvertimeApplicationExportVO> list = overtimeApplicationService.getExportList(reqVO);
ExcelUtils.write(response, "加班申请_" + LocalDate.now() + ".xls", "加班申请",
OvertimeApplicationExportVO.class, list);
}
}

View File

@@ -0,0 +1,40 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@ExcelIgnoreUnannotated
public class OvertimeApplicationExportVO {
@ExcelProperty("申请人")
private String applicantName;
@ExcelProperty("加班日期")
private LocalDate overtimeDate;
@ExcelProperty("加班时长")
private String overtimeDuration;
@ExcelProperty("加班原因")
private String overtimeReason;
@ExcelProperty("加班内容")
private String overtimeContent;
@ExcelProperty("状态")
private String statusName;
@ExcelProperty("审核人")
private String approverName;
@ExcelProperty("提交时间")
private LocalDateTime submitTime;
@ExcelProperty("审核时间")
private LocalDateTime approvalTime;
}

View File

@@ -0,0 +1,44 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
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 OvertimeApplicationPageReqVO extends PageParam {
@Schema(description = "关键词,匹配加班原因或加班内容", example = "上线")
private String keyword;
@Schema(description = "申请人姓名,模糊匹配", example = "张三")
private String applicantName;
@Schema(description = "审核人用户编号", example = "1001")
private Long approverId;
@Schema(description = "审核人姓名,模糊匹配", example = "李四")
private String approverName;
@Schema(description = "状态编码", example = "pending")
@Size(max = 32, message = "状态编码长度不能超过32个字符")
private String statusCode;
@Schema(description = "加班日期范围", example = "[2026-06-01, 2026-06-30]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate[] overtimeDate;
@Schema(description = "创建时间", example = "[2026-06-01 00:00:00, 2026-06-30 23:59:59]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,66 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 加班申请 Response VO")
@Data
public class OvertimeApplicationRespVO {
@Schema(description = "加班申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
private Long id;
@Schema(description = "申请人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
private Long applicantId;
@Schema(description = "申请人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
private String applicantName;
@Schema(description = "加班日期", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDate overtimeDate;
@Schema(description = "加班时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "1天")
private String overtimeDuration;
@Schema(description = "加班原因", requiredMode = Schema.RequiredMode.REQUIRED)
private String overtimeReason;
@Schema(description = "加班内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String overtimeContent;
@Schema(description = "审核人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
private Long approverId;
@Schema(description = "审核人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
private String approverName;
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
private String statusCode;
@Schema(description = "状态名称", example = "待审批")
private String statusName;
@Schema(description = "当前状态是否允许编辑", example = "false")
private Boolean allowEdit;
@Schema(description = "是否终态", example = "false")
private Boolean terminal;
@Schema(description = "最近一次审核意见")
private String approvalComment;
@Schema(description = "提交时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime submitTime;
@Schema(description = "最近一次审核时间")
private LocalDateTime approvalTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,37 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
@Schema(description = "管理后台 - 加班申请保存 Request VO")
@Data
public class OvertimeApplicationSaveReqVO {
@Schema(description = "加班日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-01")
@NotNull(message = "加班日期不能为空")
private LocalDate overtimeDate;
@Schema(description = "加班时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "1天")
@NotBlank(message = "加班时长不能为空")
@Size(max = 30, message = "加班时长长度不能超过30个字符")
private String overtimeDuration;
@Schema(description = "加班原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "版本上线保障")
@NotBlank(message = "加班原因不能为空")
@Size(max = 500, message = "加班原因长度不能超过500个字符")
private String overtimeReason;
@Schema(description = "加班内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "处理上线验证和问题修复")
@NotBlank(message = "加班内容不能为空")
@Size(max = 1000, message = "加班内容长度不能超过1000个字符")
private String overtimeContent;
@Schema(description = "审核人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@NotNull(message = "审核人不能为空")
private Long approverId;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 加班申请状态动作 Request VO")
@Data
public class OvertimeApplicationStatusActionReqVO {
@Schema(description = "动作原因或审核意见。是否必填以状态机配置为准;当前退回必填,撤销选填",
example = "请补充加班内容")
@Size(max = 1000, message = "动作原因长度不能超过 1000 个字符")
private String reason;
}

View File

@@ -0,0 +1,51 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 加班申请状态日志 Response VO")
@Data
public class OvertimeApplicationStatusLogRespVO {
@Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long id;
@Schema(description = "加班申请编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long applicationId;
@Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "approve")
private String actionType;
@Schema(description = "变更前状态", example = "pending")
private String fromStatus;
@Schema(description = "变更后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "approved")
private String toStatus;
@Schema(description = "原因或审核意见")
private String reason;
@Schema(description = "操作人用户编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long operatorUserId;
@Schema(description = "操作人名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String operatorName;
@Schema(description = "申请人姓名快照", requiredMode = Schema.RequiredMode.REQUIRED)
private String applicantNameSnapshot;
@Schema(description = "加班日期快照", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDate overtimeDateSnapshot;
@Schema(description = "加班时长快照", requiredMode = Schema.RequiredMode.REQUIRED)
private String overtimeDurationSnapshot;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,160 @@
package com.njcn.rdms.module.project.controller.admin.personal;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.constant.PersonalItemConstants;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemPageReqVO;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemRespVO;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogSaveReqVO;
import com.njcn.rdms.module.project.service.personal.PersonalItemService;
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService;
import io.swagger.v3.oas.annotations.Operation;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 个人事项")
@RestController
@RequestMapping("/project/personal-items")
@Validated
public class PersonalItemController {
@Resource
private PersonalItemService personalItemService;
@Resource
private ProjectExecutionService projectExecutionService;
@PostMapping
@Operation(summary = "创建个人事项")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_CREATE + "')")
public CommonResult<Long> create(@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
return success(personalItemService.createItem(reqVO));
}
@PutMapping("/{id}")
@Operation(summary = "更新个人事项")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> update(@PathVariable("id") Long id,
@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
personalItemService.updateItem(id, reqVO);
return success(true);
}
@GetMapping("/{id}")
@Operation(summary = "获取个人事项详情")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
public CommonResult<PersonalItemRespVO> get(@PathVariable("id") Long id) {
return success(personalItemService.getItemRespVO(id));
}
@GetMapping("/page")
@Operation(summary = "获取个人事项分页")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
public CommonResult<PageResult<PersonalItemRespVO>> page(@Valid PersonalItemPageReqVO reqVO) {
return success(personalItemService.getItemRespVOPage(reqVO));
}
@PostMapping("/{id}/change-status")
@Operation(summary = "变更个人事项状态")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_STATUS + "')")
public CommonResult<Boolean> changeStatus(@PathVariable("id") Long id,
@Valid @RequestBody PersonalItemStatusActionReqVO reqVO) {
personalItemService.changeStatus(id, reqVO);
return success(true);
}
@PostMapping("/{id}/worklogs")
@Operation(summary = "新增个人事项工作日志")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
public CommonResult<Long> createWorklog(@PathVariable("id") Long id,
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
return success(personalItemService.createWorklog(id, reqVO));
}
@GetMapping("/{id}/worklogs")
@Operation(summary = "获取个人事项工作日志分页")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
public CommonResult<PageResult<TaskWorklogRespVO>> getWorklogPage(@PathVariable("id") Long id,
@Valid TaskWorklogPageReqVO reqVO) {
return success(personalItemService.getWorklogPage(id, reqVO));
}
@PutMapping("/{id}/worklogs/{worklogId}")
@Operation(summary = "修改个人事项工作日志")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> updateWorklog(@PathVariable("id") Long id,
@PathVariable("worklogId") Long worklogId,
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
personalItemService.updateWorklog(id, worklogId, reqVO);
return success(true);
}
@DeleteMapping("/{id}/worklogs/{worklogId}")
@Operation(summary = "删除个人事项工作日志")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> deleteWorklog(@PathVariable("id") Long id,
@PathVariable("worklogId") Long worklogId) {
personalItemService.deleteWorklog(id, worklogId);
return success(true);
}
@DeleteMapping("/{id}/worklogs/delete-list")
@Operation(summary = "批量删除个人事项工作日志")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> deleteWorklogs(@PathVariable("id") Long id,
@RequestParam("ids") List<Long> ids) {
personalItemService.deleteWorklogs(id, ids);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除个人事项")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
personalItemService.deleteItem(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除个人事项")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
public CommonResult<Boolean> deleteList(@RequestParam("ids") List<Long> ids) {
personalItemService.deleteItems(ids);
return success(true);
}
@PostMapping("/relate-execution")
@Operation(summary = "批量个人事项关联执行")
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> relateExecution(@RequestParam("itemIds") List<Long> itemIds,
@RequestParam("executionId") Long executionId) {
personalItemService.relateExecution(itemIds, executionId);
return success(true);
}
@GetMapping("/owner/all-execution")
@Operation(summary = "获取当前登录用户负责的所有执行")
public CommonResult<List<ProjectExecutionRespVO>> getCurrentUserExecutionList() {
return success(projectExecutionService.getCurrentUserExecutionList());
}
}

View File

@@ -0,0 +1,20 @@
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
@Schema(description = "管理后台 - 个人事项生命周期动作 Response VO")
@Data
@NoArgsConstructor
public class PersonalItemLifecycleActionRespVO {
@Schema(description = "动作编码", example = "complete")
private String actionCode;
@Schema(description = "动作名称", example = "完成")
private String actionName;
@Schema(description = "是否必须填写原因", example = "true")
private Boolean needReason;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 个人事项分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class PersonalItemPageReqVO extends PageParam {
@Schema(description = "关键词,匹配个人事项标题", example = "沟通纪要")
private String keyword;
@Schema(description = "负责人用户编号;当前阶段仅支持查询本人事项", example = "3001")
private Long ownerId;
@Schema(description = "个人事项状态编码", example = "pending")
@Size(max = 32, message = "个人事项状态编码长度不能超过32个字符")
private String statusCode;
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] updateTime;
}

View File

@@ -0,0 +1,79 @@
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 个人事项 Response VO")
@Data
public class PersonalItemRespVO {
@Schema(description = "个人事项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
private Long id;
@Schema(description = "个人事项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "整理供应商沟通纪要")
private String taskTitle;
@Schema(description = "个人事项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "todo")
private String type;
@Schema(description = "负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
private Long ownerId;
@Schema(description = "负责人用户昵称", example = "小王")
private String ownerNickname;
@Schema(description = "个人事项状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
private String statusCode;
@Schema(description = "个人事项状态名称", example = "待开始")
private String statusName;
@Schema(description = "是否终态", example = "false")
private Boolean terminal;
@Schema(description = "当前状态是否允许编辑", example = "true")
private Boolean allowEdit;
@Schema(description = "当前状态可执行动作")
private List<PersonalItemLifecycleActionRespVO> availableActions;
@Schema(description = "个人事项进度", example = "60.00")
private BigDecimal progressRate;
@Schema(description = "已填报工时合计小时0.5 颗粒);逻辑删除的工时记录不计入。无记录默认为 0",
example = "8.0")
private BigDecimal totalSpentHours;
@Schema(description = "计划开始日期")
private LocalDate plannedStartDate;
@Schema(description = "计划结束日期")
private LocalDate plannedEndDate;
@Schema(description = "实际开始日期")
private LocalDate actualStartDate;
@Schema(description = "实际结束日期")
private LocalDate actualEndDate;
@Schema(description = "个人事项说明")
private String taskDesc;
@Schema(description = "最近一次状态动作原因")
private String lastStatusReason;
@Schema(description = "附件列表")
private List<AttachmentItem> attachments;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,50 @@
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - 个人事项保存 Request VO")
@Data
public class PersonalItemSaveReqVO {
@Schema(description = "执行编号,仅用于创建/编辑时补充执行成员合法性校验", example = "5001")
private Long executionId;
@Schema(description = "个人事项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "整理供应商沟通纪要")
@NotBlank(message = "个人事项标题不能为空")
@Size(max = 300, message = "个人事项标题长度不能超过300个字符")
private String taskTitle;
@Schema(description = "个人事项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "todo")
@NotBlank(message = "个人事项类型不能为空")
@Size(max = 32, message = "个人事项类型长度不能超过32个字符")
private String type;
@Schema(description = "个人事项进度0~100", example = "60.00")
@DecimalMin(value = "0.00", message = "个人事项进度不能小于 0")
@DecimalMax(value = "100.00", message = "个人事项进度不能大于 100")
private BigDecimal progressRate;
@Schema(description = "计划开始日期", example = "2026-05-20")
private LocalDate plannedStartDate;
@Schema(description = "计划结束日期", example = "2026-05-25")
private LocalDate plannedEndDate;
@Schema(description = "个人事项说明(富文本 HTML")
private String taskDesc;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 个人事项状态动作 Request VO")
@Data
public class PersonalItemStatusActionReqVO {
@Schema(description = "动作编码,如 start、complete、reopen", requiredMode = Schema.RequiredMode.REQUIRED,
example = "complete")
@NotBlank(message = "动作编码不能为空")
@Size(max = 32, message = "动作编码长度不能超过32个字符")
private String actionCode;
@Schema(description = "动作原因;是否必填由状态流转配置决定", example = "事项完成")
@Size(max = 500, message = "动作原因长度不能超过500个字符")
private String reason;
}

View File

@@ -3,7 +3,10 @@ package com.njcn.rdms.module.project.controller.admin.product;
import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils; import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductCreateWithTeamReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO; 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.ProductRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
@@ -37,9 +40,15 @@ public class ProductController {
return success(productService.createProduct(createReqVO)); return success(productService.createProduct(createReqVO));
} }
@PostMapping("/create-with-team")
@Operation(summary = "创建产品并初始化团队(原子接口)")
@PreAuthorize("@ss.hasPermission('project:product:create')")
public CommonResult<Long> createProductWithTeam(@Valid @RequestBody ProductCreateWithTeamReqVO reqVO) {
return success(productService.createProductWithTeam(reqVO));
}
@PutMapping("/update") @PutMapping("/update")
@Operation(summary = "更新产品") @Operation(summary = "更新产品")
@PreAuthorize("@ss.hasPermission('project:product:update')")
public CommonResult<Boolean> updateProduct(@Valid @RequestBody ProductSaveReqVO updateReqVO) { public CommonResult<Boolean> updateProduct(@Valid @RequestBody ProductSaveReqVO updateReqVO) {
productService.updateProduct(updateReqVO); productService.updateProduct(updateReqVO);
return success(true); return success(true);
@@ -48,23 +57,33 @@ public class ProductController {
@GetMapping("/get") @GetMapping("/get")
@Operation(summary = "获取产品详情") @Operation(summary = "获取产品详情")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024") @Parameter(name = "id", description = "产品编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('project:product:query')")
public CommonResult<ProductRespVO> getProduct(@RequestParam("id") Long id) { public CommonResult<ProductRespVO> getProduct(@RequestParam("id") Long id) {
ProductDO product = productService.getProduct(id); ProductDO product = productService.getProduct(id);
return success(BeanUtils.toBean(product, ProductRespVO.class)); return success(BeanUtils.toBean(product, ProductRespVO.class));
} }
@GetMapping("/{id}/context")
@Operation(summary = "获取产品上下文")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<ProductContextRespVO> getProductContext(@PathVariable("id") Long id) {
return success(productService.getProductContext(id));
}
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "获取产品分页") @Operation(summary = "获取产品分页")
@PreAuthorize("@ss.hasPermission('project:product:query')")
public CommonResult<PageResult<ProductRespVO>> getProductPage(@Valid ProductPageReqVO pageReqVO) { public CommonResult<PageResult<ProductRespVO>> getProductPage(@Valid ProductPageReqVO pageReqVO) {
PageResult<ProductDO> pageResult = productService.getProductPage(pageReqVO); PageResult<ProductDO> pageResult = productService.getProductPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ProductRespVO.class)); return success(BeanUtils.toBean(pageResult, ProductRespVO.class));
} }
@GetMapping("/overview-summary")
@Operation(summary = "获取产品入口页概览统计")
public CommonResult<ProductOverviewSummaryRespVO> getProductOverviewSummary() {
return success(productService.getProductOverviewSummary());
}
@PostMapping("/change-status") @PostMapping("/change-status")
@Operation(summary = "变更产品状态") @Operation(summary = "变更产品状态")
@PreAuthorize("@ss.hasPermission('project:product:status')")
public CommonResult<Boolean> changeProductStatus(@Valid @RequestBody ProductStatusActionReqVO reqVO) { public CommonResult<Boolean> changeProductStatus(@Valid @RequestBody ProductStatusActionReqVO reqVO) {
productService.changeProductStatus(reqVO); productService.changeProductStatus(reqVO);
return success(true); return success(true);
@@ -72,7 +91,6 @@ public class ProductController {
@PostMapping("/delete") @PostMapping("/delete")
@Operation(summary = "删除产品") @Operation(summary = "删除产品")
@PreAuthorize("@ss.hasPermission('project:product:delete')")
public CommonResult<Boolean> deleteProduct(@Valid @RequestBody ProductDeleteReqVO reqVO) { public CommonResult<Boolean> deleteProduct(@Valid @RequestBody ProductDeleteReqVO reqVO) {
productService.deleteProduct(reqVO); productService.deleteProduct(reqVO);
return success(true); return success(true);

View File

@@ -0,0 +1,82 @@
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;
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberUpdateReqVO;
import com.njcn.rdms.module.project.service.product.ProductMemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 产品团队")
@RestController
@RequestMapping("/project/product")
@Validated
public class ProductMemberController {
@Resource
private ProductMemberService productMemberService;
@GetMapping("/{id}/members")
@Operation(summary = "获取产品团队成员列表")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<List<ProductMemberRespVO>> getProductMemberList(@PathVariable("id") Long productId) {
return success(productMemberService.getProductMemberList(productId));
}
@PostMapping("/{id}/members")
@Operation(summary = "新增产品团队成员")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<Long> createProductMember(@PathVariable("id") Long productId,
@Valid @RequestBody ProductMemberSaveReqVO reqVO) {
return success(productMemberService.createProductMember(productId, reqVO));
}
@PostMapping("/{id}/members/batch")
@Operation(summary = "批量新增产品团队成员")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<List<Long>> batchCreateProductMembers(@PathVariable("id") Long productId,
@Valid @RequestBody ProductMemberBatchCreateReqVO reqVO) {
return success(productMemberService.batchCreateProductMembers(productId, reqVO));
}
@PutMapping("/{id}/members/{memberId}")
@Operation(summary = "调整产品团队成员角色")
public CommonResult<Boolean> updateProductMember(@PathVariable("id") Long productId,
@PathVariable("memberId") Long memberId,
@Valid @RequestBody ProductMemberUpdateReqVO reqVO) {
productMemberService.updateProductMember(productId, memberId, reqVO);
return success(true);
}
@PostMapping("/{id}/members/{memberId}/inactive")
@Operation(summary = "移出产品团队成员")
public CommonResult<Boolean> inactiveProductMember(@PathVariable("id") Long productId,
@PathVariable("memberId") Long memberId,
@Valid @RequestBody ProductMemberInactiveReqVO reqVO) {
productMemberService.inactiveProductMember(productId, memberId, reqVO);
return success(true);
}
@PostMapping("/{id}/members/batch/inactive")
@Operation(summary = "批量移出产品团队成员")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<Boolean> batchInactiveProductMembers(@PathVariable("id") Long productId,
@Valid @RequestBody ProductMemberBatchInactiveReqVO reqVO) {
productMemberService.batchInactiveProductMembers(productId, reqVO);
return success(true);
}
}

View File

@@ -0,0 +1,180 @@
package com.njcn.rdms.module.project.controller.admin.product;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
import com.njcn.rdms.module.project.service.product.ProductRequirementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
/**
* 管理后台 - 产品需求控制器
*/
@Tag(name = "管理后台 - 产品需求")
@RestController
@RequestMapping("/project/product/requirement")
@Validated
public class ProductRequirementController {
@Resource
private ProductRequirementService requirementService;
@PostMapping("/create")
@Operation(summary = "创建产品需求")
public CommonResult<Long> createRequirement(@Valid @RequestBody ProductRequirementSaveReqVO createReqVO) {
return success(requirementService.createRequirement(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新产品需求")
public CommonResult<Boolean> updateRequirement(@Valid @RequestBody ProductRequirementUpdateReqVO updateReqVO) {
requirementService.updateRequirement(updateReqVO);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获取需求详情")
@Parameter(name = "id", description = "需求编号", required = true, example = "1024")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<ProductRequirementRespVO> getRequirement(@RequestParam("id") Long id,
@RequestParam("productId") Long productId) {
return success(requirementService.getRequirement(id, productId));
}
@GetMapping("/page")
@Operation(summary = "获取需求分页列表")
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementPage(@Valid ProductRequirementPageReqVO pageReqVO) {
return success(requirementService.getRequirementPage(pageReqVO));
}
@GetMapping("/tree")
@Operation(summary = "获取需求树分页列表")
public CommonResult<PageResult<ProductRequirementRespVO>> getRequirementTree(@Valid ProductRequirementPageReqVO pageReqVO) {
return success(requirementService.getRequirementTree(pageReqVO));
}
@GetMapping("/dashboard")
@Operation(summary = "获取产品需求概览数据")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<ProductRequirementDashboardRespVO> getRequirementDashboard(@RequestParam("productId") Long productId) {
return success(requirementService.getRequirementDashboard(productId));
}
@PostMapping("/change-status")
@Operation(summary = "变更需求状态")
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProductRequirementStatusActionReqVO reqVO) {
requirementService.changeRequirementStatus(reqVO);
return success(true);
}
@PostMapping("/delete")
@Operation(summary = "删除产品需求")
public CommonResult<Boolean> deleteRequirement(@Valid @RequestBody ProductRequirementDeleteReqVO reqVO) {
requirementService.deleteRequirement(reqVO.getId(), reqVO.getProductId());
return success(true);
}
@PostMapping("/split")
@Operation(summary = "拆分产品需求")
public CommonResult<Long> splitRequirement(@Valid @RequestBody ProductRequirementSplitReqVO reqVO) {
return success(requirementService.splitRequirement(reqVO));
}
@PostMapping("/close")
@Operation(summary = "关闭产品需求")
public CommonResult<Boolean> closeRequirement(@Valid @RequestBody ProductRequirementCloseReqVO reqVO) {
requirementService.closeRequirement(reqVO);
return success(true);
}
@GetMapping("/allowed-transitions")
@Operation(summary = "获取需求可执行的状态动作列表")
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<List<ProductRequirementStatusTransitionRespVO>> getAllowedTransitions(
@RequestParam("requirementId") Long requirementId,
@RequestParam("productId") Long productId) {
return success(requirementService.getAllowedTransitions(requirementId, productId));
}
@PostMapping("/allowed-transitions/batch")
@Operation(summary = "批量获取需求可执行的状态动作列表")
public CommonResult<List<ProductRequirementAllowedTransitionBatchRespVO>> getAllowedTransitionsBatch(
@Valid @RequestBody ProductRequirementBatchReqVO reqVO) {
return success(requirementService.getAllowedTransitionsBatch(reqVO));
}
@GetMapping("/has-dispatched")
@Operation(summary = "判断产品需求是否已分流生成项目需求")
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<Boolean> hasDispatchedProjectRequirement(
@RequestParam("requirementId") Long requirementId,
@RequestParam("productId") Long productId) {
return success(requirementService.hasDispatchedProjectRequirement(requirementId, productId));
}
@PostMapping("/has-dispatched/batch")
@Operation(summary = "批量判断产品需求是否已分流生成项目需求")
public CommonResult<List<ProductRequirementHasDispatchedBatchRespVO>> hasDispatchedProjectRequirementBatch(
@Valid @RequestBody ProductRequirementBatchReqVO reqVO) {
return success(requirementService.hasDispatchedProjectRequirementBatch(reqVO));
}
@GetMapping("/dispatched-project-link")
@Operation(summary = "获取产品需求分流后对应的项目需求跳转链接")
@Parameter(name = "productRequirementId", description = "产品需求编号", required = true, example = "1024")
public CommonResult<ProductRequirementDispatchedProjectLinkRespVO> getDispatchedProjectLink(
@RequestParam("productRequirementId") Long productRequirementId) {
return success(requirementService.getDispatchedProjectLink(productRequirementId));
}
@PostMapping("/module/create")
@Operation(summary = "创建需求模块")
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
return success(requirementService.createRequirementModule(reqVO));
}
@PutMapping("/module/update")
@Operation(summary = "更新需求模块")
public CommonResult<Boolean> updateRequirementModule(@Valid @RequestBody ProductRequirementModuleReqVO reqVO) {
requirementService.updateRequirementModule(reqVO);
return success(true);
}
@PostMapping("/module/delete")
@Operation(summary = "删除需求模块")
public CommonResult<Boolean> deleteRequirementModule(@Valid @RequestBody ProductRequirementModuleDeleteReqVO reqVO) {
requirementService.deleteRequirementModule(reqVO.getId(), reqVO.getProductId());
return success(true);
}
@GetMapping("/module/tree")
@Operation(summary = "获取需求模块树")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<List<ProductRequirementModuleRespVO>> getRequirementModuleTree(@RequestParam("productId") Long productId) {
return success(requirementService.getRequirementModuleTree(productId));
}
@GetMapping("/status/dict")
@Operation(summary = "获取需求所有状态字典")
public CommonResult<List<ProductRequirementStatusDictRespVO>> getRequirementStatusDict() {
return success(requirementService.getRequirementStatusDict());
}
@GetMapping("/status/dict/terminal")
@Operation(summary = "获取需求终止态状态字典")
public CommonResult<List<ProductRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
return success(requirementService.getRequirementTerminalStatusDict());
}
}

View File

@@ -0,0 +1,71 @@
package com.njcn.rdms.module.project.controller.admin.product;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityPageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelinePageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelineRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingRespVO;
import com.njcn.rdms.module.project.service.product.ProductService;
import com.njcn.rdms.module.project.service.product.ProductSettingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 产品设置")
@RestController
@RequestMapping("/project/product")
@Validated
public class ProductSettingController {
@Resource
private ProductSettingService productSettingService;
@Resource
private ProductService productService;
@GetMapping("/{id}/settings")
@Operation(summary = "获取产品设置")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<ProductSettingRespVO> getProductSettings(@PathVariable("id") Long id) {
return success(productSettingService.getProductSettings(id));
}
@GetMapping("/{id}/activities")
@Operation(summary = "获取产品动态")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<PageResult<ProductActivityRespVO>> getProductActivities(@PathVariable("id") Long id,
@Valid ProductActivityPageReqVO reqVO) {
return success(productSettingService.getProductActivities(id, reqVO));
}
@GetMapping("/{id}/activities/page")
@Operation(summary = "获取产品动态时间线分页")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<PageResult<ProductActivityTimelineRespVO>> getProductActivityTimelinePage(
@PathVariable("id") Long id, @Valid ProductActivityTimelinePageReqVO reqVO) {
return success(productSettingService.getProductActivityTimelinePage(id, reqVO));
}
@PutMapping("/{id}/settings/base-info")
@Operation(summary = "更新产品设置基础信息")
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
public CommonResult<Boolean> updateProductBaseInfo(@PathVariable("id") Long id,
@Valid @RequestBody ProductSettingBaseInfoUpdateReqVO reqVO) {
productService.updateProductBaseInfo(id, reqVO);
return success(true);
}
}

View File

@@ -0,0 +1,31 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.activity;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 产品动态分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProductActivityPageReqVO extends PageParam {
@Schema(description = "动态类型", example = "status")
@Size(max = 16, message = "动态类型长度不能超过16个字符")
private String activityType;
@Schema(description = "动作编码", example = "pause")
@Size(max = 32, message = "动作编码长度不能超过32个字符")
private String actionType;
@Schema(description = "操作时间区间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] operateTime;
}

View File

@@ -0,0 +1,45 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.activity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 产品动态 Response VO")
@Data
public class ProductActivityRespVO {
@Schema(description = "动态类型", example = "status")
private String type;
@Schema(description = "动作编码", example = "pause")
private String actionType;
@Schema(description = "动作名称", example = "暂停")
private String actionName;
@Schema(description = "原状态", example = "active")
private String fromStatus;
@Schema(description = "目标状态", example = "paused")
private String toStatus;
@Schema(description = "动作原因", example = "资源不足")
private String reason;
@Schema(description = "操作人用户编号", example = "1024")
private Long operatorUserId;
@Schema(description = "操作人名称", example = "张三")
private String operatorName;
@Schema(description = "操作时间", example = "2026-04-21 12:00:00")
private LocalDateTime operateTime;
@Schema(description = "展示摘要", example = "张三执行了【暂停】:资源不足")
private String summary;
@Schema(description = "补充详情")
private String details;
}

View File

@@ -0,0 +1,35 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.activity;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 产品动态时间线分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProductActivityTimelinePageReqVO extends PageParam {
@Schema(description = "动态类型", example = "status")
@Size(max = 16, message = "动态类型长度不能超过16个字符")
private String activityType;
@Schema(description = "动作编码数组")
private List<@Size(max = 32, message = "动作编码长度不能超过32个字符") String> actionTypes;
@Schema(description = "开始时间", example = "2026-03-24 00:00:00")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime startTime;
@Schema(description = "结束时间", example = "2026-04-23 23:59:59")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime endTime;
}

View File

@@ -0,0 +1,54 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.activity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 产品动态时间线 Response VO")
@Data
public class ProductActivityTimelineRespVO {
@Schema(description = "动态唯一标识", example = "status:11")
private String id;
@Schema(description = "动态类型", example = "status")
private String type;
@Schema(description = "动作编码", example = "pause")
private String actionType;
@Schema(description = "动作名称", example = "暂停")
private String actionName;
@Schema(description = "操作人用户编号", example = "1024")
private Long operatorUserId;
@Schema(description = "操作人名称", example = "张三")
private String operatorName;
@Schema(description = "目标成员用户编号,仅 member 类型返回", example = "2043945809271713793")
private Long targetUserId;
@Schema(description = "目标成员名称,仅 member 类型返回,读取缓存实时转换", example = "张三")
private String targetUserName;
@Schema(description = "发生时间", example = "2026-04-21 12:00:00")
private LocalDateTime occurredAt;
@Schema(description = "展示摘要", example = "张三执行了【暂停】:资源不足")
private String summary;
@Schema(description = "动作原因", example = "资源不足")
private String reason;
@Schema(description = "原状态", example = "active")
private String fromStatus;
@Schema(description = "目标状态", example = "paused")
private String toStatus;
@Schema(description = "补充详情")
private String details;
}

View File

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

View File

@@ -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<Long> memberIds;
@Schema(description = "移出原因,单一字符串应用于本批所有成员", example = "组织架构调整,本批成员退出当前产品团队")
@Size(max = 500, message = "移出原因长度不能超过500个字符")
private String reason;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 产品团队成员移出 Request VO")
@Data
public class ProductMemberInactiveReqVO {
@Schema(description = "移出原因", example = "已退出当前产品协作")
@Size(max = 500, message = "移出原因长度不能超过500个字符")
private String reason;
}

View File

@@ -0,0 +1,51 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
@Schema(description = "管理后台 - 产品团队成员 Response VO")
@Data
public class ProductMemberRespVO {
@Schema(description = "团队关系编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long userId;
@Schema(description = "用户昵称", example = "小王")
private String userNickname;
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002001")
private Long roleId;
@Schema(description = "角色名称", example = "产品经理")
private String roleName;
@Schema(description = "角色编码", example = "product_manager")
private String roleCode;
@Schema(description = "是否当前产品经理", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean managerFlag;
@Schema(description = "状态0有效 1失效", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;
@Schema(description = "加入时间")
private LocalDateTime joinedTime;
@Schema(description = "退出时间")
private LocalDateTime leftTime;
@Schema(description = "备注", example = "当前负责需求收敛")
private String remark;
@ArraySchema(schema = @Schema(description = "非主角色的中文名列表,多角色场景使用(如同人 manager + creator单角色时为空数组", example = "产品创建者"))
private List<String> additionalRoleNames = Collections.emptyList();
}

View File

@@ -0,0 +1,30 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 产品团队成员新增 Request VO")
@Data
public class ProductMemberSaveReqVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "用户编号不能为空")
private Long userId;
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002001")
@NotNull(message = "角色编号不能为空")
private Long roleId;
@Schema(description = "备注", example = "来自产品团队维护")
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
@Schema(description = "原产品经理用户编号,仅切换产品经理时传递", example = "2048")
private Long previousManagerUserId;
@Schema(description = "原产品经理交接后的角色编号,仅切换产品经理时传递", example = "3100000002002")
private Long previousManagerRoleId;
}

View File

@@ -0,0 +1,30 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 产品团队成员更新 Request VO")
@Data
public class ProductMemberUpdateReqVO {
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002002")
@NotNull(message = "角色编号不能为空")
private Long roleId;
@Schema(description = "备注", example = "调整为产品观察者")
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
@Schema(description = "变更原因", example = "职责调整")
@Size(max = 500, message = "变更原因长度不能超过500个字符")
private String reason;
@Schema(description = "原产品经理用户编号,仅切换产品经理时传递", example = "2048")
private Long previousManagerUserId;
@Schema(description = "原产品经理交接后的角色编号,仅切换产品经理时传递", example = "3100000002002")
private Long previousManagerRoleId;
}

View File

@@ -0,0 +1,25 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 产品上下文导航 Response VO")
@Data
public class ProductContextNavRespVO {
@Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3201")
private Long id;
@Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "概览")
private String name;
@Schema(description = "菜单路径", example = "/project/product/overview")
private String path;
@Schema(description = "菜单图标", example = "mdi:view-dashboard-outline")
private String icon;
@Schema(description = "显示顺序", example = "10")
private Integer sort;
}

View File

@@ -0,0 +1,28 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 产品上下文中的当前产品摘要 Response VO")
@Data
public class ProductContextProductRespVO {
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPD2026001")
private String code;
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
private String directionCode;
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
private String name;
@Schema(description = "产品经理用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long managerUserId;
@Schema(description = "产品状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
private String statusCode;
}

View File

@@ -0,0 +1,26 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Schema(description = "管理后台 - 产品上下文 Response VO")
@Data
@NoArgsConstructor
public class ProductContextRespVO {
@Schema(description = "当前产品摘要")
private ProductContextProductRespVO currentProduct;
@Schema(description = "当前用户在该产品下的角色信息")
private ProductContextRoleRespVO currentRole;
@Schema(description = "当前产品下可见导航集合")
private List<ProductContextNavRespVO> navs;
@Schema(description = "当前产品下按钮权限码集合")
private List<String> buttons;
}

View File

@@ -0,0 +1,29 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Collections;
import java.util.List;
@Schema(description = "管理后台 - 产品上下文中的当前角色 Response VO")
@Data
public class ProductContextRoleRespVO {
@Schema(description = "对象角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3201")
private Long roleId;
@Schema(description = "对象角色编码(主角色 code权限判断兼容字段", example = "product_manager")
private String roleCode;
@Schema(description = "对象角色名称(主角色 name", example = "产品经理")
private String roleName;
@Schema(description = "是否游客上下文(隐式 observer 兜底时为 true", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean guestFlag;
@ArraySchema(schema = @Schema(description = "非主角色的中文名列表,多角色场景使用;单角色时为空数组", example = "创建者"))
private List<String> additionalRoleNames = Collections.emptyList();
}

View File

@@ -0,0 +1,52 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 产品创建原子接口请求 VOPOST /project/product/create-with-team
*
* <p>由前端"产品两步向导"一次性提交:产品基础资料 + 初始团队成员。
* 后端必须在同一事务内完成全部写入,任一步失败整体回滚。
*/
@Schema(description = "管理后台 - 产品创建含初始团队Request VO")
@Data
public class ProductCreateWithTeamReqVO {
/**
* 产品基础资料。沿用 {@link ProductSaveReqVO} 字段约束(同 POST /create
*/
@Schema(description = "产品基础资料", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "产品基础资料不能为空")
@Valid
private ProductSaveReqVO product;
/**
* 初始团队成员列表。
*
* <p>必须包含一条 {@code userId == product.managerUserId} 的产品经理成员,
* 由前端在打开第 2 步时按 role code = {@code product_manager} 反查 roleId 后聚合提交。
* 后端不再根据 {@code product.managerUserId} 自动追加经理成员。
*/
@Schema(description = "初始团队成员(含产品经理本人)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "初始团队成员不能为空")
@Valid
private List<ProductMemberSaveReqVO> members;
/**
* 关心人 user ID 列表(创建时手动添加,可选)。
*
* <p>跟 members 是平行字段watcher 不参与团队管理,只是被授予"产品关心人"角色product_watcher
* 数据可见性。允许跟 members 的 user 重叠(多角色合法);后端按 (user, object, role) 三元组写入,
* 重复跳过 / INACTIVE 复活,业务侧不强校验。
*/
@Schema(description = "关心人用户ID列表可选可与团队成员重叠", example = "101,102")
private List<Long> watcherUserIds;
}

View File

@@ -24,4 +24,9 @@ public class ProductDeleteReqVO {
@Size(max = 500, message = "删除原因长度不能超过500个字符") @Size(max = 500, message = "删除原因长度不能超过500个字符")
private String reason; private String reason;
@Schema(description = "删除确认口令,当前固定输入 DELETE", requiredMode = Schema.RequiredMode.REQUIRED, example = "DELETE")
@NotBlank(message = "删除确认口令不能为空")
@Size(max = 32, message = "删除确认口令长度不能超过32个字符")
private String confirmText;
} }

View File

@@ -0,0 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Map;
@Schema(description = "管理后台 - 产品入口页概览统计 Response VO")
@Data
public class ProductOverviewSummaryRespVO {
@Schema(description = "产品状态数量,按当前启用的产品状态模型返回")
private Map<String, Long> statusCounts;
}

View File

@@ -2,7 +2,7 @@ package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import com.njcn.rdms.framework.common.pojo.PageParam; import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.dict.validation.InDict; import com.njcn.rdms.framework.dict.validation.InDict;
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; import com.njcn.rdms.module.system.enums.DictTypeConstants;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
@@ -21,8 +21,8 @@ public class ProductPageReqVO extends PageParam {
@Schema(description = "关键词,匹配产品编码或产品名称", example = "CNPD2026001") @Schema(description = "关键词,匹配产品编码或产品名称", example = "CNPD2026001")
private String keyword; private String keyword;
@Schema(description = "产品方向字典值", example = "embedded") @Schema(description = "产品方向字典值", example = "direction_value")
@InDict(type = ProjectDictTypeConstants.PRODUCT_DIRECTION) @InDict(type = DictTypeConstants.OBJECT_DIRECTION)
private String directionCode; private String directionCode;
@Schema(description = "产品经理用户编号", example = "1024") @Schema(description = "产品经理用户编号", example = "1024")

View File

@@ -15,7 +15,7 @@ public class ProductRespVO {
@Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPD2026001") @Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPD2026001")
private String code; private String code;
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "embedded") @Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
private String directionCode; private String directionCode;
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台") @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
@@ -33,9 +33,6 @@ public class ProductRespVO {
@Schema(description = "最近一次状态动作原因", example = "阶段性暂停") @Schema(description = "最近一次状态动作原因", example = "阶段性暂停")
private String lastStatusReason; private String lastStatusReason;
@Schema(description = "备注", example = "预留")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime; private LocalDateTime createTime;

View File

@@ -1,7 +1,7 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product; package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import com.njcn.rdms.framework.dict.validation.InDict; import com.njcn.rdms.framework.dict.validation.InDict;
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; import com.njcn.rdms.module.system.enums.DictTypeConstants;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@@ -19,10 +19,10 @@ public class ProductSaveReqVO {
@Size(max = 64, message = "产品编码长度不能超过64个字符") @Size(max = 64, message = "产品编码长度不能超过64个字符")
private String code; private String code;
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "embedded") @Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
@NotBlank(message = "产品方向不能为空") @NotBlank(message = "产品方向不能为空")
@Size(max = 32, message = "产品方向长度不能超过32个字符") @Size(max = 32, message = "产品方向长度不能超过32个字符")
@InDict(type = ProjectDictTypeConstants.PRODUCT_DIRECTION) @InDict(type = DictTypeConstants.OBJECT_DIRECTION)
private String directionCode; private String directionCode;
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台") @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
@@ -37,8 +37,4 @@ public class ProductSaveReqVO {
@Schema(description = "产品描述", example = "面向研发管理的一体化产品") @Schema(description = "产品描述", example = "面向研发管理的一体化产品")
private String description; private String description;
@Schema(description = "备注", example = "预留")
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
} }

View File

@@ -0,0 +1,18 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 产品需求批量可执行动作 Response VO")
@Data
public class ProductRequirementAllowedTransitionBatchRespVO {
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long requirementId;
@Schema(description = "可执行动作列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<ProductRequirementStatusTransitionRespVO> transitions;
}

View File

@@ -0,0 +1,22 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 产品需求批量查询 Request VO")
@Data
public class ProductRequirementBatchReqVO {
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品编号不能为空")
private Long productId;
@Schema(description = "需求编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]")
@NotEmpty(message = "需求编号列表不能为空")
private List<Long> requirementIds;
}

View File

@@ -0,0 +1,29 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 管理后台 - 产品需求关闭 Request VO
*/
@Schema(description = "管理后台 - 产品需求关闭 Request VO")
@Data
public class ProductRequirementCloseReqVO {
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "需求编号不能为空")
private Long id;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品ID不能为空")
private Long productId;
@Schema(description = "关闭原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "需求已完成验收")
@NotBlank(message = "关闭原因不能为空")
@Size(max = 255, message = "关闭原因长度不能超过255个字符")
private String reason;
}

View File

@@ -0,0 +1,42 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 管理后台 - 产品需求概览最近变化 Response VO
*/
@Schema(description = "管理后台 - 产品需求概览最近变化 Response VO")
@Data
public class ProductRequirementDashboardRecentChangeRespVO {
@Schema(description = "前端列表唯一键", requiredMode = Schema.RequiredMode.REQUIRED, example = "requirement:create:2048")
private String id;
@Schema(description = "产品需求ID", example = "1003")
private Long requirementId;
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "补充对象首页需求池统计接口")
private String title;
@Schema(description = "动作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "create")
private String actionType;
@Schema(description = "动作名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "需求新增")
private String actionLabel;
@Schema(description = "展示内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "当前状态:待评审")
private String content;
@Schema(description = "发生时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime occurredAt;
@Schema(description = "操作人用户ID", example = "1024")
private Long operatorUserId;
@Schema(description = "操作人名称快照", example = "张三")
private String operatorName;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 管理后台 - 产品需求概览 Response VO
*/
@Schema(description = "管理后台 - 产品需求概览 Response VO")
@Data
public class ProductRequirementDashboardRespVO {
@Schema(description = "需求池统计")
private ProductRequirementDashboardSummaryRespVO summary;
@Schema(description = "需求池最近重要变化")
private List<ProductRequirementDashboardRecentChangeRespVO> recentChanges;
}

View File

@@ -0,0 +1,37 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 管理后台 - 产品需求概览统计 Response VO
*/
@Schema(description = "管理后台 - 产品需求概览统计 Response VO")
@Data
public class ProductRequirementDashboardSummaryRespVO {
@Schema(description = "需求总量,包括根需求和子需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "18")
private Long total;
@Schema(description = "待处理需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
private Long todo;
@Schema(description = "待认领需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long pendingClaim;
@Schema(description = "待评审需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long pendingReview;
@Schema(description = "待指派需求数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long pendingDispatch;
@Schema(description = "完成需求数,已验收和已关闭计入完成", requiredMode = Schema.RequiredMode.REQUIRED, example = "6")
private Long completed;
@Schema(description = "完成率,四舍五入后的百分比整数", requiredMode = Schema.RequiredMode.REQUIRED, example = "33")
private Integer completionRate;
@Schema(description = "高优先待处理需求数P0/P1 且处于待处理状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long highPriorityTodo;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 管理后台 - 产品需求删除 Request VO
*/
@Schema(description = "管理后台 - 产品需求删除 Request VO")
@Data
public class ProductRequirementDeleteReqVO {
@Schema(description = "需求ID编辑时传入", example = "1024")
private Long id;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品ID不能为空")
private Long productId;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 管理后台 - 产品需求指派后项目需求跳转链接 Response VO
*/
@Schema(description = "管理后台 - 产品需求指派后项目需求跳转链接 Response VO")
@Data
public class ProductRequirementDispatchedProjectLinkRespVO {
@Schema(description = "项目需求ID", example = "10086")
private Long projectRequirementId;
@Schema(description = "关联项目ID", example = "8888")
private Long projectId;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 产品需求批量指派状态 Response VO")
@Data
public class ProductRequirementHasDispatchedBatchRespVO {
@Schema(description = "需求编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long requirementId;
@Schema(description = "是否已指派生成项目需求", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean hasDispatched;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 管理后台 - 产品需求模块删除 Request VO
*/
@Schema(description = "管理后台 - 产品需求模块删除 Request VO")
@Data
public class ProductRequirementModuleDeleteReqVO {
@Schema(description = "模块ID编辑时传入", example = "1024")
private Long id;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品ID不能为空")
private Long productId;
}

View File

@@ -0,0 +1,40 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 管理后台 - 产品需求模块保存 Request VO
*/
@Schema(description = "管理后台 - 产品需求模块保存 Request VO")
@Data
public class ProductRequirementModuleReqVO {
@Schema(description = "模块ID编辑时传入", example = "1024")
private Long id;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品ID不能为空")
private Long productId;
@Schema(description = "父模块ID0表示顶级", example = "0")
private Long parentId;
@Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能")
@NotBlank(message = "模块名称不能为空")
@Size(max = 100, message = "模块名称长度不能超过100个字符")
private String moduleName;
@Schema(description = "模块说明", example = "产品核心功能模块")
private String remark;
@Schema(description = "图标", example = "icon-function")
private String icon;
@Schema(description = "排序值", example = "0")
private Integer sort;
}

View File

@@ -0,0 +1,39 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 管理后台 - 产品需求模块 Response VO
*/
@Schema(description = "管理后台 - 产品需求模块 Response VO")
@Data
public class ProductRequirementModuleRespVO {
@Schema(description = "模块ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "父模块ID0表示顶级", example = "0")
private Long parentId;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long productId;
@Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "核心功能")
private String moduleName;
@Schema(description = "模块说明", example = "产品核心功能模块")
private String remark;
@Schema(description = "图标", example = "icon-function")
private String icon;
@Schema(description = "排序值", example = "0")
private Integer sort;
@Schema(description = "子模块列表")
private List<ProductRequirementModuleRespVO> children;
}

View File

@@ -0,0 +1,49 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.baomidou.mybatisplus.annotation.TableField;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 管理后台 - 产品需求分页 Request VO
*/
@Schema(description = "管理后台 - 产品需求分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ProductRequirementPageReqVO extends PageParam {
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long productId;
@Schema(description = "所属模块ID", example = "1024")
private Long moduleId;
@Schema(description = "所属模块ID列表包含子模块用于IN查询", example = "[1024, 1025]")
private List<Long> moduleIds;
@Schema(description = "父需求ID查询子需求时使用", example = "1024")
private Long parentId;
@Schema(description = "标题关键词", example = "模块")
private String title;
@Schema(description = "需求分类字典值", example = "function")
private String category;
@Schema(description = "优先级0低 1中 2高 3紧急", example = "1")
private Integer priority;
@Schema(description = "状态编码", example = "pending_dispatch")
private String statusCode;
@Schema(description = "当前处理人用户编号", example = "1024")
private Long currentHandlerUserId;
@Schema(description = "来源类型manual:手工新增, work_order:工单流转)", example = "manual")
private String sourceType;
}

View File

@@ -0,0 +1,99 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.baomidou.mybatisplus.annotation.TableField;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 管理后台 - 产品需求 Response VO
*/
@Schema(description = "管理后台 - 产品需求 Response VO")
@Data
public class ProductRequirementRespVO {
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "父需求ID0 表示顶级需求", example = "0")
private Long parentId;
@Schema(description = "所属模块ID", example = "1024")
private Long moduleId;
@Schema(description = "是否需要评审0不需要1需要", example = "0")
private Integer reviewRequired;
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
private String title;
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
private String description;
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
private String category;
@Schema(description = "需求分类名称", example = "功能需求")
private String categoryName;
@Schema(description = "需求来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "manual")
private String sourceType;
@Schema(description = "来源业务ID", example = "1024")
private Long sourceBizId;
@Schema(description = "优先级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer priority;
@Schema(description = "优先级名称", example = "")
private String priorityName;
@Schema(description = "当前状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending_dispatch")
private String statusCode;
@Schema(description = "当前状态名称", example = "待指派")
private String statusName;
@Schema(description = "最近一次状态动作原因", example = "需求全部结束")
private String lastStatusReason;
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long proposerId;
@Schema(description = "提出人用户姓名", example = "张三")
private String proposerNickname;
@Schema(description = "预期完成时间", example = "2026-05-31")
private LocalDate expectedTime;
@Schema(description = "当前处理人用户ID", example = "1024")
private Long currentHandlerUserId;
@Schema(description = "当前处理人姓名", example = "李四")
private String currentHandlerUserNickname;
@Schema(description = "默认实现项目编号", example = "1024")
private Long implementProjectId;
@Schema(description = "实现项目名称", example = "NPQS-10086")
private String implementProjectName;
@Schema(description = "排序值", example = "0")
private Integer sort;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
@Schema(description = "附件列表")
private List<AttachmentItem> attachments;
@Schema(description = "子需求列表,树形结构")
private List<ProductRequirementRespVO> children;
}

View File

@@ -0,0 +1,78 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
/**
* 管理后台 - 产品需求保存 Request VO
*/
@Schema(description = "管理后台 - 产品需求保存 Request VO")
@Data
public class ProductRequirementSaveReqVO {
@Schema(description = "需求ID", example = "1024")
private Long id;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品编号不能为空")
private Long productId;
@Schema(description = "所属模块ID为空时归入全部需求模块", example = "1024")
private Long moduleId;
@Schema(description = "是否需要评审0不需要1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "是否需要评审不能为空")
private Integer reviewRequired;
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
@NotBlank(message = "需求标题不能为空")
@Size(max = 200, message = "需求标题长度不能超过200个字符")
private String title;
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
private String description;
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
@NotBlank(message = "需求分类不能为空")
@Size(max = 64, message = "需求分类长度不能超过64个字符")
private String category;
@Schema(description = "优先级0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "优先级不能为空")
private Integer priority;
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "提出人不能为空")
private Long proposerId;
@Schema(description = "提出人姓名", example = "张三")
private String proposerNickname;
@Schema(description = "预期完成时间", example = "2026-05-31")
private LocalDate expectedTime;
@Schema(description = "当前处理人用户ID", example = "1024")
private Long currentHandlerUserId;
@Schema(description = "当前处理人姓名", example = "李四")
private String currentHandlerUserNickname;
@Schema(description = "默认实现项目编号", example = "1024")
private Long implementProjectId;
@Schema(description = "排序值,越小越靠前", example = "0")
private Integer sort;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

View File

@@ -0,0 +1,79 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
/**
* 管理后台 - 产品需求拆分 Request VO
*/
@Schema(description = "管理后台 - 产品需求拆分 Request VO")
@Data
public class ProductRequirementSplitReqVO {
@Schema(description = "父需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "父需求编号不能为空")
private Long parentId;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品编号不能为空")
private Long productId;
@Schema(description = "所属模块ID为空时归入全部需求模块", example = "1024")
private Long moduleId;
@Schema(description = "是否需要评审0不需要1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "是否需要评审不能为空")
private Integer reviewRequired;
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
@NotBlank(message = "需求标题不能为空")
@Size(max = 200, message = "需求标题长度不能超过200个字符")
private String title;
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
private String description;
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
@NotBlank(message = "需求分类不能为空")
@Size(max = 64, message = "需求分类长度不能超过64个字符")
private String category;
@Schema(description = "优先级0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "优先级不能为空")
private Integer priority;
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "提出人不能为空")
private Long proposerId;
@Schema(description = "提出人姓名", example = "张三")
private String proposerNickname;
@Schema(description = "预期完成时间", example = "2026-05-31")
private LocalDate expectedTime;
@Schema(description = "当前处理人用户ID", example = "1024")
private Long currentHandlerUserId;
@Schema(description = "当前处理人姓名", example = "李四")
private String currentHandlerUserNickname;
@Schema(description = "默认实现项目编号", example = "1024")
private Long implementProjectId;
@Schema(description = "排序值,越小越靠前", example = "0")
private Integer sort;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

View File

@@ -0,0 +1,35 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 管理后台 - 产品需求状态变更 Request VO
* 注意:需求不直接关联产品,通过模块间接关联
*/
@Schema(description = "管理后台 - 产品需求状态变更 Request VO")
@Data
public class ProductRequirementStatusActionReqVO {
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "需求ID不能为空")
private Long id;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品编号不能为空")
private Long productId;
@Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "dispatch")
@NotBlank(message = "动作编码不能为空")
@Size(max = 32, message = "动作编码长度不能超过32个字符")
private String actionCode;
@Schema(description = "状态变更原因", example = "需求全部完成")
private String reason;
@Schema(description = "关联项目编号dispatch动作时可选", example = "1024")
private Long implementProjectId;
}

View File

@@ -0,0 +1,31 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 管理后台 - 产品需求状态字典 Response VO
*/
@Schema(description = "管理后台 - 产品需求状态字典 Response VO")
@Data
public class ProductRequirementStatusDictRespVO {
@Schema(description = "状态编码", example = "pending_claim")
private String statusCode;
@Schema(description = "状态名称", example = "待认领")
private String statusName;
@Schema(description = "排序值", example = "1")
private Integer sort;
@Schema(description = "是否初始状态", example = "true")
private Boolean initialFlag;
@Schema(description = "是否终态", example = "false")
private Boolean terminalFlag;
@Schema(description = "是否允许编辑", example = "true")
private Boolean allowEdit;
}

View File

@@ -0,0 +1,30 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 管理后台 - 产品需求状态可执行动作 Response VO
*/
@Schema(description = "管理后台 - 产品需求状态可执行动作 Response VO")
@Data
@NoArgsConstructor
public class ProductRequirementStatusTransitionRespVO {
@Schema(description = "动作编码", example = "dispatch")
private String actionCode;
@Schema(description = "动作名称", example = "明确指派/拆分")
private String actionName;
@Schema(description = "目标状态编码", example = "implementing")
private String toStatusCode;
@Schema(description = "目标状态名称", example = "实施中")
private String toStatusName;
@Schema(description = "是否必须填写原因", example = "false")
private Boolean needReason;
}

View File

@@ -0,0 +1,79 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.requirement;
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
/**
* 管理后台 - 产品需求编辑 Request VO
*/
@Schema(description = "管理后台 - 产品需求编辑 Request VO")
@Data
public class ProductRequirementUpdateReqVO {
@Schema(description = "需求ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "需求ID不能为空")
private Long id;
@Schema(description = "所属产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "产品ID不能为空")
private Long productId;
@Schema(description = "所属模块ID为空时归入全部需求模块", example = "1024")
private Long moduleId;
@Schema(description = "是否需要评审0不需要1需要", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "是否需要评审不能为空")
private Integer reviewRequired;
@Schema(description = "需求标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "支持需求模块化管理")
@NotBlank(message = "需求标题不能为空")
@Size(max = 200, message = "需求标题长度不能超过200个字符")
private String title;
@Schema(description = "需求描述,支持富文本", example = "<p>详细描述需求内容</p>")
private String description;
@Schema(description = "需求分类字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "function")
@NotBlank(message = "需求分类不能为空")
@Size(max = 64, message = "需求分类长度不能超过64个字符")
private String category;
@Schema(description = "优先级0低、1中、2高、3紧急", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "优先级不能为空")
private Integer priority;
@Schema(description = "提出人用户ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "提出人不能为空")
private Long proposerId;
@Schema(description = "提出人姓名", example = "张三")
private String proposerNickname;
@Schema(description = "预期完成时间", example = "2026-05-31")
private LocalDate expectedTime;
@Schema(description = "当前处理人用户ID", example = "1024")
private Long currentHandlerUserId;
@Schema(description = "当前处理人姓名", example = "李四")
private String currentHandlerUserNickname;
@Schema(description = "默认实现项目编号", example = "1024")
private Long implementProjectId;
@Schema(description = "排序值,越小越靠前", example = "0")
private Integer sort;
@Schema(description = "附件列表")
@Valid
private List<AttachmentItem> attachments;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
@Schema(description = "管理后台 - 产品设置生命周期动作 Response VO")
@Data
@NoArgsConstructor
public class ProductSettingActionRespVO {
@Schema(description = "动作编码", example = "archive")
private String actionCode;
@Schema(description = "动作名称", example = "归档")
private String actionName;
@Schema(description = "是否必须填写原因", example = "true")
private Boolean needReason;
}

View File

@@ -0,0 +1,39 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
@Schema(description = "管理后台 - 产品设置基础信息 Response VO")
@Data
@NoArgsConstructor
public class ProductSettingBaseInfoRespVO {
@Schema(description = "产品编号", example = "1024")
private Long id;
@Schema(description = "产品编码", example = "CNPD2026001")
private String code;
@Schema(description = "产品方向字典值", example = "direction_value")
private String directionCode;
@Schema(description = "产品名称", example = "统一交付平台")
private String name;
@Schema(description = "产品经理用户编号", example = "10001")
private Long managerUserId;
@Schema(description = "产品经理昵称", example = "张三")
private String managerUserNickname;
@Schema(description = "产品描述", example = "产品描述")
private String description;
@Schema(description = "产品状态编码", example = "active")
private String statusCode;
@Schema(description = "最近一次状态动作原因", example = "恢复正常推进")
private String lastStatusReason;
}

View File

@@ -0,0 +1,28 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
import com.njcn.rdms.framework.dict.validation.InDict;
import com.njcn.rdms.module.system.enums.DictTypeConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 产品设置基础信息更新 Request VO")
@Data
public class ProductSettingBaseInfoUpdateReqVO {
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "direction_value")
@NotBlank(message = "产品方向不能为空")
@Size(max = 32, message = "产品方向长度不能超过32个字符")
@InDict(type = DictTypeConstants.OBJECT_DIRECTION)
private String directionCode;
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
@NotBlank(message = "产品名称不能为空")
@Size(max = 128, message = "产品名称长度不能超过128个字符")
private String name;
@Schema(description = "产品描述", example = "面向研发管理的一体化产品")
private String description;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Schema(description = "管理后台 - 产品设置生命周期 Response VO")
@Data
@NoArgsConstructor
public class ProductSettingLifecycleRespVO {
@Schema(description = "当前状态编码", example = "active")
private String statusCode;
@Schema(description = "当前状态名称", example = "启用")
private String statusName;
@Schema(description = "最近一次状态动作原因", example = "恢复正常推进")
private String lastStatusReason;
@Schema(description = "是否终态", example = "false")
private Boolean terminal;
@Schema(description = "是否允许编辑", example = "true")
private Boolean allowEdit;
@Schema(description = "当前状态可执行动作")
private List<ProductSettingActionRespVO> availableActions;
}

View File

@@ -0,0 +1,18 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.setting;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
@Schema(description = "管理后台 - 产品设置 Response VO")
@Data
@NoArgsConstructor
public class ProductSettingRespVO {
@Schema(description = "产品基础信息")
private ProductSettingBaseInfoRespVO baseInfo;
@Schema(description = "产品生命周期信息")
private ProductSettingLifecycleRespVO lifecycle;
}

View File

@@ -0,0 +1,107 @@
package com.njcn.rdms.module.project.controller.admin.project;
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.project.vo.project.ProjectContextRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectCreateWithTeamReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectOverviewSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectStatusActionReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.service.project.ProjectService;
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 java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 项目管理")
@RestController
@RequestMapping("/project/project")
@Validated
public class ProjectController {
@Resource
private ProjectService projectService;
@PostMapping("/create")
@Operation(summary = "创建项目")
@PreAuthorize("@ss.hasPermission('project:project:create')")
public CommonResult<Long> createProject(@Valid @RequestBody ProjectSaveReqVO createReqVO) {
return success(projectService.createProject(createReqVO));
}
@PostMapping("/create-with-team")
@Operation(summary = "创建项目并初始化团队(原子接口)")
@PreAuthorize("@ss.hasPermission('project:project:create')")
public CommonResult<Long> createProjectWithTeam(@Valid @RequestBody ProjectCreateWithTeamReqVO reqVO) {
return success(projectService.createProjectWithTeam(reqVO));
}
@PutMapping("/update")
@Operation(summary = "更新项目")
public CommonResult<Boolean> updateProject(@Valid @RequestBody ProjectSaveReqVO updateReqVO) {
projectService.updateProject(updateReqVO);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获取项目详情")
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
public CommonResult<ProjectRespVO> getProject(@RequestParam("id") Long id) {
return success(projectService.getProjectDetail(id));
}
@GetMapping("/{id}/context")
@Operation(summary = "获取项目上下文")
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
public CommonResult<ProjectContextRespVO> getProjectContext(@PathVariable("id") Long id) {
return success(projectService.getProjectContext(id));
}
@GetMapping("/page")
@Operation(summary = "获取项目分页")
public CommonResult<PageResult<ProjectRespVO>> getProjectPage(@Valid ProjectPageReqVO pageReqVO) {
PageResult<ProjectDO> pageResult = projectService.getProjectPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ProjectRespVO.class));
}
@GetMapping("/list-by-product")
@Operation(summary = "根据产品编号获取该产品下的全部项目")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
public CommonResult<List<ProjectRespVO>> getProjectListByProductId(@RequestParam("productId") Long productId) {
return success(projectService.getProjectListByProductId(productId));
}
@GetMapping("/overview-summary")
@Operation(summary = "获取项目入口页概览统计")
public CommonResult<ProjectOverviewSummaryRespVO> getProjectOverviewSummary() {
return success(projectService.getProjectOverviewSummary());
}
@PostMapping("/change-status")
@Operation(summary = "变更项目状态")
public CommonResult<Boolean> changeProjectStatus(@Valid @RequestBody ProjectStatusActionReqVO reqVO) {
projectService.changeProjectStatus(reqVO);
return success(true);
}
@PostMapping("/delete")
@Operation(summary = "删除项目")
public CommonResult<Boolean> deleteProject(@Valid @RequestBody ProjectDeleteReqVO reqVO) {
projectService.deleteProject(reqVO);
return success(true);
}
}

View File

@@ -0,0 +1,82 @@
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;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberUpdateReqVO;
import com.njcn.rdms.module.project.service.project.ProjectMemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 项目成员")
@RestController
@RequestMapping("/project/project")
@Validated
public class ProjectMemberController {
@Resource
private ProjectMemberService projectMemberService;
@GetMapping("/{id}/members")
@Operation(summary = "获取项目成员列表")
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
public CommonResult<List<ProjectMemberRespVO>> getProjectMemberList(@PathVariable("id") Long projectId) {
return success(projectMemberService.getProjectMemberList(projectId));
}
@PostMapping("/{id}/members")
@Operation(summary = "新增项目成员")
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
public CommonResult<Long> createProjectMember(@PathVariable("id") Long projectId,
@Valid @RequestBody ProjectMemberSaveReqVO reqVO) {
return success(projectMemberService.createProjectMember(projectId, reqVO));
}
@PostMapping("/{id}/members/batch")
@Operation(summary = "批量新增项目成员")
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
public CommonResult<List<Long>> batchCreateProjectMembers(@PathVariable("id") Long projectId,
@Valid @RequestBody ProjectMemberBatchCreateReqVO reqVO) {
return success(projectMemberService.batchCreateProjectMembers(projectId, reqVO));
}
@PutMapping("/{id}/members/{memberId}")
@Operation(summary = "调整项目成员角色")
public CommonResult<Boolean> updateProjectMember(@PathVariable("id") Long projectId,
@PathVariable("memberId") Long memberId,
@Valid @RequestBody ProjectMemberUpdateReqVO reqVO) {
projectMemberService.updateProjectMember(projectId, memberId, reqVO);
return success(true);
}
@PostMapping("/{id}/members/{memberId}/inactive")
@Operation(summary = "移出项目成员")
public CommonResult<Boolean> inactiveProjectMember(@PathVariable("id") Long projectId,
@PathVariable("memberId") Long memberId,
@Valid @RequestBody ProjectMemberInactiveReqVO reqVO) {
projectMemberService.inactiveProjectMember(projectId, memberId, reqVO);
return success(true);
}
@PostMapping("/{id}/members/batch/inactive")
@Operation(summary = "批量移出项目成员")
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
public CommonResult<Boolean> batchInactiveProjectMembers(@PathVariable("id") Long projectId,
@Valid @RequestBody ProjectMemberBatchInactiveReqVO reqVO) {
projectMemberService.batchInactiveProjectMembers(projectId, reqVO);
return success(true);
}
}

View File

@@ -0,0 +1,162 @@
package com.njcn.rdms.module.project.controller.admin.project;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementAllowedTransitionBatchRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSplitReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusDictRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusTransitionRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementUpdateReqVO;
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
/**
* 管理后台 - 项目需求控制器
*/
@Tag(name = "管理后台 - 项目需求")
@RestController
@RequestMapping("/project/project/requirement")
@Validated
public class ProjectRequirementController {
@Resource
private ProjectRequirementService requirementService;
@PostMapping("/create")
@Operation(summary = "创建项目需求")
public CommonResult<Long> createRequirement(@Valid @RequestBody ProjectRequirementSaveReqVO createReqVO) {
return success(requirementService.createRequirement(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新项目需求")
public CommonResult<Boolean> updateRequirement(@Valid @RequestBody ProjectRequirementUpdateReqVO updateReqVO) {
requirementService.updateRequirement(updateReqVO);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获取需求详情")
@Parameter(name = "id", description = "需求编号", required = true, example = "1024")
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
public CommonResult<ProjectRequirementRespVO> getRequirement(@RequestParam("id") Long id,
@RequestParam("projectId") Long projectId) {
return success(requirementService.getRequirement(id, projectId));
}
@GetMapping("/page")
@Operation(summary = "获取需求分页列表")
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementPage(@Valid ProjectRequirementPageReqVO pageReqVO) {
return success(requirementService.getRequirementPage(pageReqVO));
}
@GetMapping("/tree")
@Operation(summary = "获取需求树分页列表")
public CommonResult<PageResult<ProjectRequirementRespVO>> getRequirementTree(@Valid ProjectRequirementPageReqVO pageReqVO) {
return success(requirementService.getRequirementTree(pageReqVO));
}
@PostMapping("/change-status")
@Operation(summary = "变更需求状态")
public CommonResult<Boolean> changeRequirementStatus(@Valid @RequestBody ProjectRequirementStatusActionReqVO reqVO) {
requirementService.changeRequirementStatus(reqVO);
return success(true);
}
@PostMapping("/delete")
@Operation(summary = "删除项目需求")
public CommonResult<Boolean> deleteRequirement(@Valid @RequestBody ProjectRequirementDeleteReqVO reqVO) {
requirementService.deleteRequirement(reqVO.getId(), reqVO.getProjectId());
return success(true);
}
@PostMapping("/split")
@Operation(summary = "拆分项目需求")
public CommonResult<Long> splitRequirement(@Valid @RequestBody ProjectRequirementSplitReqVO reqVO) {
return success(requirementService.splitRequirement(reqVO));
}
@PostMapping("/close")
@Operation(summary = "关闭项目需求")
public CommonResult<Boolean> closeRequirement(@Valid @RequestBody ProjectRequirementCloseReqVO reqVO) {
requirementService.closeRequirement(reqVO);
return success(true);
}
@GetMapping("/allowed-transitions")
@Operation(summary = "获取需求可执行的状态动作列表")
@Parameter(name = "requirementId", description = "需求编号", required = true, example = "1024")
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
public CommonResult<List<ProjectRequirementStatusTransitionRespVO>> getAllowedTransitions(
@RequestParam("requirementId") Long requirementId,
@RequestParam("projectId") Long projectId) {
return success(requirementService.getAllowedTransitions(requirementId, projectId));
}
@PostMapping("/allowed-transitions/batch")
@Operation(summary = "批量获取需求可执行的状态动作列表")
public CommonResult<List<ProjectRequirementAllowedTransitionBatchRespVO>> getAllowedTransitionsBatch(
@Valid @RequestBody ProjectRequirementBatchReqVO reqVO) {
return success(requirementService.getAllowedTransitionsBatch(reqVO));
}
@PostMapping("/module/create")
@Operation(summary = "创建需求模块")
public CommonResult<Long> createRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) {
return success(requirementService.createRequirementModule(reqVO));
}
@PutMapping("/module/update")
@Operation(summary = "更新需求模块")
public CommonResult<Boolean> updateRequirementModule(@Valid @RequestBody ProjectRequirementModuleReqVO reqVO) {
requirementService.updateRequirementModule(reqVO);
return success(true);
}
@PostMapping("/module/delete")
@Operation(summary = "删除需求模块")
public CommonResult<Boolean> deleteRequirementModule(@Valid @RequestBody ProjectRequirementModuleDeleteReqVO reqVO) {
requirementService.deleteRequirementModule(reqVO.getId(), reqVO.getProjectId());
return success(true);
}
@GetMapping("/module/tree")
@Operation(summary = "获取需求模块树")
@Parameter(name = "projectId", description = "项目编号", required = true, example = "1024")
public CommonResult<List<ProjectRequirementModuleRespVO>> getRequirementModuleTree(@RequestParam("projectId") Long projectId) {
return success(requirementService.getRequirementModuleTree(projectId));
}
@GetMapping("/status/dict")
@Operation(summary = "获取需求所有状态字典")
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementStatusDict() {
return success(requirementService.getRequirementStatusDict());
}
@GetMapping("/status/dict/terminal")
@Operation(summary = "获取需求终止态状态字典")
public CommonResult<List<ProjectRequirementStatusDictRespVO>> getRequirementTerminalStatusDict() {
return success(requirementService.getRequirementTerminalStatusDict());
}
}

View File

@@ -0,0 +1,70 @@
package com.njcn.rdms.module.project.controller.admin.project.execution;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeInactiveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogRespVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeRespVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeSaveReqVO;
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionAssigneeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 执行协办人")
@RestController
@RequestMapping("/project/project/{projectId}/executions/{executionId}")
@Validated
public class ProjectExecutionAssigneeController {
@Resource
private ProjectExecutionAssigneeService projectExecutionAssigneeService;
@GetMapping("/assignees")
@Operation(summary = "获取执行协办人列表(仅当前活跃)")
public CommonResult<List<ExecutionAssigneeRespVO>> getExecutionAssigneeList(@PathVariable("projectId") Long projectId,
@PathVariable("executionId") Long executionId) {
return success(projectExecutionAssigneeService.getExecutionAssigneeList(projectId, executionId));
}
@PostMapping("/assignees")
@Operation(summary = "新增执行协办人B 模型 - 每次新增一段)")
public CommonResult<Long> createExecutionAssignee(@PathVariable("projectId") Long projectId,
@PathVariable("executionId") Long executionId,
@Valid @RequestBody ExecutionAssigneeSaveReqVO reqVO) {
return success(projectExecutionAssigneeService.createExecutionAssignee(projectId, executionId, reqVO));
}
@PostMapping("/assignees/{assigneeId}/inactive")
@Operation(summary = "失效执行协办人(永久保留 removedReason")
public CommonResult<Boolean> inactiveExecutionAssignee(@PathVariable("projectId") Long projectId,
@PathVariable("executionId") Long executionId,
@PathVariable("assigneeId") Long assigneeId,
@Valid @RequestBody ExecutionAssigneeInactiveReqVO reqVO) {
projectExecutionAssigneeService.inactiveExecutionAssignee(projectId, executionId, assigneeId, reqVO);
return success(true);
}
@GetMapping("/assignee-logs")
@Operation(summary = "获取执行协办人变更历史(分页)")
public CommonResult<PageResult<ExecutionAssigneeLogRespVO>> getExecutionAssigneeLogPage(
@PathVariable("projectId") Long projectId,
@PathVariable("executionId") Long executionId,
@Valid ExecutionAssigneeLogPageReqVO reqVO) {
return success(projectExecutionAssigneeService.getExecutionAssigneeLogPage(projectId, executionId, reqVO));
}
}

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