diff --git a/.image/common/pms-feature.png b/.image/common/pms-feature.png new file mode 100644 index 000000000..25e12776b Binary files /dev/null and b/.image/common/pms-feature.png differ diff --git a/.image/common/pms-preview.png b/.image/common/pms-preview.png new file mode 100644 index 000000000..c7a85aa76 Binary files /dev/null and b/.image/common/pms-preview.png differ diff --git a/README.md b/README.md index 303c5121f..b866c8331 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ | 【完整版】[yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [`master`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk17/) 分支 | [`master-jdk25`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk25/) 分支 | | 【精简版】[yudao-cloud-mini](https://gitee.com/yudaocode/yudao-cloud-mini) | [`master`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master-jdk17/) 分支 | [`master-jdk25`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master-jdk25/) 分支 | -* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、HRM、FMS、IM 即时通讯、AI 大模型、IoT 物联网等功能 -* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、HRM、FMS、IM 即时通讯、AI 大模型、IoT 物联网等功能 +* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、HRM、FMS、PMS、IM 即时通讯、AI 大模型、IoT 物联网等功能 +* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、HRM、FMS、PMS、IM 即时通讯、AI 大模型、IoT 物联网等功能 可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】 @@ -105,7 +105,7 @@ 团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。 -项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、HRM 人力资源系统、FMS 财务系统、CMS 系统、HIS 系统、支付系统、IM 即时通讯、微信公众号、微信小程序等等。 +项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、HRM 人力资源系统、FMS 财务系统、PMS 项目管理系统、CMS 系统、HIS 系统、支付系统、IM 即时通讯、微信公众号、微信小程序等等。 ## 🐼 内置功能 @@ -115,7 +115,7 @@ * 通用模块(必选):系统功能、基础设施 * 通用模块(可选):工作流程、支付系统、数据报表、会员中心 -* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、HRM 人力资源管理、FMS 财务管理、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏 +* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、HRM 人力资源管理、FMS 财务管理、PMS 项目管理、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏 > 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 > @@ -311,6 +311,14 @@ ![预览图](/.image/common/fms-preview.png) +### PMS 项目管理系统 + +演示地址: + +![功能图](/.image/common/pms-feature.png) + +![预览图](/.image/common/pms-preview.png) + ### AI 大模型 演示地址: @@ -361,6 +369,7 @@ | `yudao-module-wms` | WMS 系统的 Module 模块 | | `yudao-module-hrm` | HRM 人力资源管理的 Module 模块 | | `yudao-module-fms` | FMS 财务管理的 Module 模块 | +| `yudao-module-pms` | PMS 项目管理的 Module 模块 | | `yudao-module-im` | IM 即时通讯的 Module 模块 | | `yudao-module-ai` | AI 大模型的 Module 模块 | | `yudao-module-iot` | IoT 物联网的 Module 模块 | diff --git a/pom.xml b/pom.xml index 762c31731..bedfda5f3 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ yudao-module-im yudao-module-hrm yudao-module-fms + yudao-module-pms diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index c31b0d63b..b8fd2e995 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -4204,7 +4204,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8052, '知识库创建', 'pms:kb:library:create', 3, 2, 8050, '', '', '', '', 0, b'1', b'1', b'1', '1', '2026-08-19 00:28:42', '1', '2026-08-23 16:21:39', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8053, '知识库修改', 'pms:kb:library:update', 3, 3, 8050, '', '', '', '', 0, b'1', b'1', b'1', '1', '2026-08-19 00:28:42', '1', '2026-08-23 16:21:39', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8054, '知识库删除', 'pms:kb:library:delete', 3, 4, 8050, '', '', '', '', 0, b'1', b'1', b'1', '1', '2026-08-19 00:28:42', '1', '2026-08-23 16:21:39', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8060, '我的关注', 'pms:kb:knowledge-library:query', 2, 40, 8090, 'collection', 'ep:star', 'pms/kb/collection/index', 'PmsKnowledgeCollection', 0, b'1', b'1', b'0', '1', '2026-08-19 03:05:49', '1', '2026-08-23 16:12:56', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8060, '我的关注', 'pms:kb:knowledge-library:query', 2, 40, 8090, 'favorite', 'ep:star', 'pms/kb/favorite/index', 'PmsKnowledgeFavorite', 0, b'1', b'1', b'0', '1', '2026-08-19 03:05:49', '1', '2026-08-23 16:12:56', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8070, '最近使用', 'pms:kb:knowledge-library:query', 2, 50, 8090, 'recent', 'ep:clock', 'pms/kb/recent/index', 'PmsKnowledgeRecent', 0, b'1', b'1', b'0', '1', '2026-08-19 03:05:49', '1', '2026-08-23 16:12:56', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8080, '项目中心', '', 1, 10, 8000, 'pm', 'ep:grid', NULL, NULL, 0, b'1', b'1', b'1', '1', '2026-08-19 11:42:30', '1', '2026-08-20 09:05:58', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8081, '归档项目', 'pms:pm:project:query', 2, 30, 8080, 'project/archive', 'ep:box', 'pms/pm/project/archive/index', 'PmsProjectArchive', 0, b'1', b'0', b'0', '1', '2026-08-19 11:42:30', '1', '2026-08-27 10:01:06', b'0'); diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java index 517fd603e..2702c703a 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java @@ -43,6 +43,12 @@ public class BannerApplicationRunner implements ApplicationRunner { System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); // MES 系统 System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]"); + // HRM 人力资源管理系统 + System.out.println("[HRM 人力资源管理系统 yudao-module-hrm - 教程][参考 https://cloud.iocoder.cn/hrm/build/ 开启]"); + // FMS 财务管理系统 + System.out.println("[FMS 财务管理系统 yudao-module-fms - 教程][参考 https://cloud.iocoder.cn/fms/build/ 开启]"); + // PMS 项目管理系统 + System.out.println("[PMS 项目管理系统 yudao-module-pms - 教程][参考 https://cloud.iocoder.cn/pms/build/ 开启]"); // IM 即时通讯 System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]"); // 微信公众号 diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index d17e06370..d787e7a37 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -417,43 +417,61 @@ public class GlobalExceptionHandler { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); } - // 6. WMS 仓库管理系统 + // 6. HRM 人力资源管理系统 + if (message.contains("hrm_")) { + log.error("[HRM 人力资源管理系统 yudao-module-hrm - 表结构未导入][参考 https://cloud.iocoder.cn/hrm/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[HRM 人力资源管理系统 yudao-module-hrm - 表结构未导入][参考 https://cloud.iocoder.cn/hrm/build/ 开启]"); + } + // 7. FMS 财务管理系统 + if (message.contains("fms_")) { + log.error("[FMS 财务管理系统 yudao-module-fms - 表结构未导入][参考 https://cloud.iocoder.cn/fms/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[FMS 财务管理系统 yudao-module-fms - 表结构未导入][参考 https://cloud.iocoder.cn/fms/build/ 开启]"); + } + // 8. PMS 项目管理系统 + if (message.contains("pms_")) { + log.error("[PMS 项目管理系统 yudao-module-pms - 表结构未导入][参考 https://cloud.iocoder.cn/pms/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[PMS 项目管理系统 yudao-module-pms - 表结构未导入][参考 https://cloud.iocoder.cn/pms/build/ 开启]"); + } + // 9. WMS 仓库管理系统 if (message.contains("wms_")) { log.error("[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]"); } - // 7. CRM 系统 + // 10. CRM 系统 if (message.contains("crm_")) { log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); } - // 8. MES 系统 + // 11. MES 系统 if (message.contains("mes_")) { log.error("[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]"); } - // 9. IM 即时通讯 + // 12. IM 即时通讯 if (message.contains("im_")) { log.error("[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]"); } - // 10. 支付平台 + // 13. 支付平台 if (message.contains("pay_")) { log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); } - // 11. AI 大模型 + // 14. AI 大模型 if (message.contains("ai_")) { log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); } - // 12. IoT 物联网 + // 15. IoT 物联网 if (message.contains("iot_")) { log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), diff --git a/yudao-gateway/src/main/resources/application.yaml b/yudao-gateway/src/main/resources/application.yaml index 96515035d..b3a2b3958 100644 --- a/yudao-gateway/src/main/resources/application.yaml +++ b/yudao-gateway/src/main/resources/application.yaml @@ -227,6 +227,13 @@ spring: - Path=/admin-api/fms/** filters: - RewritePath=/admin-api/fms/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs + ## pms-server 服务 + - id: pms-admin-api # 路由的编号 + uri: grayLb://pms-server + predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 + - Path=/admin-api/pms/** + filters: + - RewritePath=/admin-api/pms/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs x-forwarded: prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀 default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组 @@ -301,6 +308,9 @@ knife4j: - name: fms-server service-name: fms-server url: /admin-api/fms/v3/api-docs + - name: pms-server + service-name: pms-server + url: /admin-api/pms/v3/api-docs --- #################### 芋道相关配置 #################### diff --git a/yudao-module-pms/pom.xml b/yudao-module-pms/pom.xml new file mode 100644 index 000000000..14333d7bc --- /dev/null +++ b/yudao-module-pms/pom.xml @@ -0,0 +1,24 @@ + + + + cn.iocoder.cloud + yudao + ${revision} + + 4.0.0 + + yudao-module-pms-api + yudao-module-pms-server + + yudao-module-pms + pom + + ${project.artifactId} + + pms 包下,项目管理(Project Management System)。 + 例如说:项目、工作事项、迭代、看板、工时和知识库等等 + + + diff --git a/yudao-module-pms/yudao-module-pms-api/pom.xml b/yudao-module-pms/yudao-module-pms-api/pom.xml new file mode 100644 index 000000000..ae6c5c95a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/pom.xml @@ -0,0 +1,33 @@ + + + + cn.iocoder.cloud + yudao-module-pms + ${revision} + + 4.0.0 + yudao-module-pms-api + jar + + ${project.artifactId} + + pms 模块 API,暴露给其它模块调用 + + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/api/package-info.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/api/package-info.java new file mode 100644 index 000000000..163c18d5a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/api/package-info.java @@ -0,0 +1,4 @@ +/** + * pms API 包,定义暴露给其它模块的 API + */ +package cn.iocoder.yudao.module.pms.api; diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/ApiConstants.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/ApiConstants.java new file mode 100644 index 000000000..9147202f3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/ApiConstants.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.pms.enums; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; + +/** + * API 相关的枚举 + * + * @author 芋道源码 + */ +public class ApiConstants { + + /** 服务名,需要和 spring.application.name 保持一致。 */ + public static final String NAME = "pms-server"; + + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/pms"; + + public static final String VERSION = "1.0.0"; + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/DictTypeConstants.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/DictTypeConstants.java new file mode 100644 index 000000000..09f460044 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/DictTypeConstants.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pms.enums; + +/** + * PMS 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String PMS_WORK_ITEM_PRIORITY = "pms_work_item_priority"; // PMS 工作项优先级 + String PMS_WORK_ITEM_DEFECT_TYPE = "pms_work_item_defect_type"; // PMS 工作项缺陷类型 + String PMS_PROJECT_TYPE = "pms_project_type"; // PMS 项目类型 + String PMS_PROJECT_STATUS = "pms_project_status"; // PMS 项目状态 + String PMS_PROJECT_LEVEL = "pms_project_level"; // PMS 项目优先级 + String PMS_PROJECT_GROUP_TYPE = "pms_project_group_type"; // PMS 项目分组类型 + String PMS_PROJECT_SCENE_TYPE = "pms_project_scene_type"; // PMS 项目列表场景 + String PMS_PROJECT_SORT_TYPE = "pms_project_sort_type"; // PMS 项目排序类型 + String PMS_PROJECT_MEMBER_LEVEL = "pms_project_member_level"; // PMS 项目成员权限级别 + String PMS_ITERATION_STATUS = "pms_iteration_status"; // PMS 迭代状态 + String PMS_WORK_ITEM_TYPE = "pms_work_item_type"; // PMS 工作项类型 + String PMS_WORK_ITEM_STATUS_TYPE = "pms_work_item_status_type"; // PMS 工作项语义状态 + String PMS_WORK_ITEM_LIFECYCLE_STATUS = "pms_work_item_lifecycle_status"; // PMS 工作项生命周期状态 + String PMS_KNOWLEDGE_DOCUMENT_STATUS = "pms_knowledge_document_status"; // PMS 知识文档状态 + String PMS_KNOWLEDGE_CONTENT_LEVEL = "pms_knowledge_content_level"; // PMS 知识内容协作等级 + String PMS_KNOWLEDGE_GROUP_TYPE = "pms_knowledge_group_type"; // PMS 知识库分组类型 + String PMS_KNOWLEDGE_LIBRARY_MEMBER_LEVEL = "pms_knowledge_library_member_level"; // PMS 知识库成员等级 + String PMS_KNOWLEDGE_OBJECT_TYPE = "pms_knowledge_object_type"; // PMS 知识对象类型 + String PMS_KNOWLEDGE_DOCUMENT_TYPE = "pms_knowledge_document_type"; // PMS 知识文档类型 + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/ErrorCodeConstants.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/ErrorCodeConstants.java new file mode 100644 index 000000000..c7daa87b9 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/ErrorCodeConstants.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.pms.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * PMS 错误码常量 + * + * PMS 系统,使用 1-053-000-000 段 + * + * @author 芋道源码 + */ +public interface ErrorCodeConstants { + + // ========== 项目 1-053-100-000 ========== + ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1_053_100_000, "项目不存在"); + ErrorCode PROJECT_ACCESS_DENIED = new ErrorCode(1_053_100_001, "无权访问该项目"); + ErrorCode PROJECT_ADMIN_REQUIRED = new ErrorCode(1_053_100_002, "只有项目管理员可以执行该操作"); + ErrorCode PROJECT_STATUS_INVALID = new ErrorCode(1_053_100_004, "当前项目状态不允许执行该操作"); + ErrorCode PROJECT_WRITE_ACCESS_DENIED = new ErrorCode(1_053_100_005, "当前项目角色无编辑权限"); + ErrorCode PROJECT_ANNOUNCEMENT_NOT_EXISTS = new ErrorCode(1_053_100_006, "项目公告不存在"); + ErrorCode PROJECT_OWNER_REQUIRED = new ErrorCode(1_053_100_007, "只有项目拥有者可以执行该操作"); + + // ========== 项目成员 1-053-105-000 ========== + ErrorCode PROJECT_MEMBER_NOT_EXISTS = new ErrorCode(1_053_105_000, "项目成员不存在"); + ErrorCode PROJECT_MEMBER_CREATOR_CANNOT_REMOVE = new ErrorCode(1_053_105_001, "项目创建人不能移出项目"); + ErrorCode PROJECT_MEMBER_OWNER_LEVEL_CANNOT_ASSIGN = new ErrorCode(1_053_105_002, + "项目拥有者级别不能直接分配"); + ErrorCode PROJECT_MEMBER_OWNER_CANNOT_EXIT = new ErrorCode(1_053_105_003, "项目拥有者不能退出项目"); + + // ========== 项目分组 1-053-110-000 ========== + ErrorCode PROJECT_GROUP_NOT_EXISTS = new ErrorCode(1_053_110_000, "项目分组不存在"); + ErrorCode PROJECT_GROUP_NAME_DUPLICATE = new ErrorCode(1_053_110_001, "项目分组名称已存在"); + ErrorCode PROJECT_GROUP_DEFAULT_CANNOT_MODIFY = new ErrorCode(1_053_110_002, "默认项目分组不允许修改"); + ErrorCode PROJECT_GROUP_DEFAULT_CANNOT_DELETE = new ErrorCode(1_053_110_003, "默认项目分组不允许删除"); + + // ========== 项目模板 1-053-112-000 ========== + ErrorCode PROJECT_TEMPLATE_NOT_EXISTS = new ErrorCode(1_053_112_000, "项目模板不存在"); + ErrorCode PROJECT_TEMPLATE_NAME_DUPLICATE = new ErrorCode(1_053_112_001, "同一项目类型下的模板名称已存在"); + ErrorCode PROJECT_TEMPLATE_CONFIG_INVALID = new ErrorCode(1_053_112_002, "项目模板配置无效:{}"); + + // ========== 项目迭代 1-053-115-000 ========== + ErrorCode ITERATION_NOT_EXISTS = new ErrorCode(1_053_115_000, "项目迭代不存在"); + ErrorCode ITERATION_STATUS_INVALID = new ErrorCode(1_053_115_002, "当前迭代状态不允许执行该操作"); + + // ========== 工作项 1-053-120-000 ========== + ErrorCode WORK_ITEM_NOT_EXISTS = new ErrorCode(1_053_120_000, "工作项不存在"); + ErrorCode WORK_ITEM_TYPE_INVALID = new ErrorCode(1_053_120_001, "当前项目不支持该工作项类型"); + ErrorCode WORK_ITEM_STATUS_NOT_EXISTS = new ErrorCode(1_053_120_002, "工作项状态不存在"); + ErrorCode WORK_ITEM_STATUS_INVALID = new ErrorCode(1_053_120_003, "工作项状态与项目或类型不匹配"); + ErrorCode WORK_ITEM_TIME_INVALID = new ErrorCode(1_053_120_004, "工作项开始时间必须早于截止时间"); + ErrorCode WORK_ITEM_ITERATION_INVALID = new ErrorCode(1_053_120_005, "所属迭代与工作项项目不匹配"); + ErrorCode WORK_ITEM_PARENT_INVALID = new ErrorCode(1_053_120_006, "父工作项与当前工作项不匹配"); + ErrorCode WORK_ITEM_RELATED_REQUIREMENT_INVALID = new ErrorCode(1_053_120_007, "关联需求与当前工作项不匹配"); + ErrorCode WORK_ITEM_DEFECT_TYPE_INVALID = new ErrorCode(1_053_120_008, "只有缺陷工作项可以设置缺陷类型"); + ErrorCode WORK_ITEM_SORT_INVALID = new ErrorCode(1_053_120_009, "工作项看板排序数据无效"); + ErrorCode WORK_ITEM_STATUS_NAME_DUPLICATE = new ErrorCode(1_053_120_010, "工作项状态名称已存在"); + ErrorCode WORK_ITEM_STATUS_DEFAULT_CANNOT_DELETE = new ErrorCode(1_053_120_011, "工作项初始状态不能删除"); + ErrorCode WORK_ITEM_STATUS_TRANSFER_INVALID = new ErrorCode(1_053_120_012, "工作项状态迁移目标无效"); + ErrorCode WORK_ITEM_STATUS_SORT_INVALID = new ErrorCode(1_053_120_013, "工作项状态排序数据无效"); + ErrorCode WORK_ITEM_WORK_LOG_NOT_EXISTS = new ErrorCode(1_053_120_014, "工作项工时记录不存在"); + ErrorCode WORK_ITEM_WORK_LOG_INVALID = new ErrorCode(1_053_120_015, "工时记录与工作项不匹配"); + ErrorCode WORK_ITEM_LABEL_NOT_EXISTS = new ErrorCode(1_053_120_016, "工作项标签不存在"); + ErrorCode WORK_ITEM_LABELS_INVALID = new ErrorCode(1_053_120_017, "工作项标签数据无效"); + ErrorCode WORK_ITEM_COMMENT_NOT_EXISTS = new ErrorCode(1_053_120_018, "工作项评论不存在"); + ErrorCode WORK_ITEM_COMMENT_REPLY_INVALID = new ErrorCode(1_053_120_019, "回复的评论与工作项不匹配"); + ErrorCode WORK_ITEM_COMMENT_ACCESS_DENIED = new ErrorCode(1_053_120_020, "只能修改或删除自己的评论"); + ErrorCode WORK_ITEM_LIFECYCLE_STATUS_INVALID = new ErrorCode(1_053_120_021, "工作项当前生命周期状态不支持该操作"); + ErrorCode WORK_ITEM_DELETE_STATUS_INVALID = new ErrorCode(1_053_120_022, "只有回收站中的工作项才能彻底删除"); + + // ========== 知识库 1-053-130-000 ========== + ErrorCode KNOWLEDGE_LIBRARY_NOT_EXISTS = new ErrorCode(1_053_130_000, "知识库不存在"); + ErrorCode KNOWLEDGE_LIBRARY_ACCESS_DENIED = new ErrorCode(1_053_130_001, "无权访问该知识库"); + ErrorCode KNOWLEDGE_LIBRARY_WRITE_ACCESS_DENIED = new ErrorCode(1_053_130_002, "无权编辑该知识库"); + ErrorCode KNOWLEDGE_LIBRARY_ADMIN_REQUIRED = new ErrorCode(1_053_130_003, "只有知识库管理员可以执行该操作"); + ErrorCode KNOWLEDGE_LIBRARY_CREATOR_CANNOT_REMOVE = new ErrorCode(1_053_130_004, "知识库创建人不能移出知识库"); + ErrorCode KNOWLEDGE_LIBRARY_MEMBERS_INVALID = new ErrorCode(1_053_130_005, "知识库成员数据无效"); + ErrorCode KNOWLEDGE_FOLDER_NOT_EXISTS = new ErrorCode(1_053_130_006, "知识库文件夹不存在"); + ErrorCode KNOWLEDGE_FOLDER_PARENT_INVALID = new ErrorCode(1_053_130_007, "父文件夹与知识库不匹配"); + ErrorCode KNOWLEDGE_FOLDER_MOVE_INVALID = new ErrorCode(1_053_130_008, "文件夹不能移动到自身或其子文件夹中"); + ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_053_130_009, "知识库文档不存在"); + ErrorCode KNOWLEDGE_DOCUMENT_PARENT_INVALID = new ErrorCode(1_053_130_010, "父文档与知识库不匹配"); + ErrorCode KNOWLEDGE_DOCUMENT_FOLDER_INVALID = new ErrorCode(1_053_130_011, "文档文件夹与知识库不匹配"); + ErrorCode KNOWLEDGE_DOCUMENT_MOVE_INVALID = new ErrorCode(1_053_130_012, "文档不能移动到自身或其子文档中"); + ErrorCode KNOWLEDGE_DOCUMENT_STATUS_INVALID = new ErrorCode(1_053_130_013, "文档状态无效"); + ErrorCode KNOWLEDGE_DOCUMENT_TYPE_INVALID = new ErrorCode(1_053_130_014, "文档类型无效"); + ErrorCode KNOWLEDGE_DOCUMENT_MOVE_TARGET_INVALID = new ErrorCode(1_053_130_015, "文档移动目标只能选择文件夹或父文档之一"); + ErrorCode KNOWLEDGE_GROUP_NOT_EXISTS = new ErrorCode(1_053_130_020, "知识库分组不存在"); + ErrorCode KNOWLEDGE_GROUP_NAME_DUPLICATE = new ErrorCode(1_053_130_021, "知识库分组名称已存在"); + ErrorCode KNOWLEDGE_GROUP_DEFAULT_CANNOT_MODIFY = new ErrorCode(1_053_130_022, "默认知识库分组不允许修改"); + ErrorCode KNOWLEDGE_GROUP_DEFAULT_CANNOT_DELETE = new ErrorCode(1_053_130_023, "默认知识库分组不允许删除"); + ErrorCode KNOWLEDGE_GROUP_SORT_DUPLICATE = new ErrorCode(1_053_130_024, "知识库分组排序列表存在重复分组"); + ErrorCode KNOWLEDGE_DOCUMENT_LABEL_NOT_EXISTS = new ErrorCode(1_053_130_030, "知识库文档标签不存在"); + ErrorCode KNOWLEDGE_RECYCLE_RECORD_NOT_EXISTS = new ErrorCode(1_053_130_040, "知识库回收站记录不存在"); + ErrorCode KNOWLEDGE_RECYCLE_TYPE_INVALID = new ErrorCode(1_053_130_041, "知识库回收站对象类型无效"); + ErrorCode KNOWLEDGE_INTERACTION_OBJECT_INVALID = new ErrorCode(1_053_130_050, "知识对象不存在或类型不匹配"); + ErrorCode KNOWLEDGE_FAVORITE_DUPLICATE = new ErrorCode(1_053_130_051, "该知识对象已关注"); + ErrorCode KNOWLEDGE_DOCUMENT_SHARE_ALREADY_OPEN = new ErrorCode(1_053_130_060, "该文档已开启分享"); + ErrorCode KNOWLEDGE_DOCUMENT_SHARE_INVALID = new ErrorCode(1_053_130_061, "文档分享不存在或已失效"); + ErrorCode KNOWLEDGE_CONTENT_PERMISSION_NOT_EXISTS = new ErrorCode(1_053_130_070, "知识内容协作权限不存在"); + ErrorCode KNOWLEDGE_CONTENT_ACCESS_DENIED = new ErrorCode(1_053_130_071, "无权访问该知识内容"); + ErrorCode KNOWLEDGE_CONTENT_WRITE_ACCESS_DENIED = new ErrorCode(1_053_130_072, "无权编辑该知识内容"); + ErrorCode KNOWLEDGE_CONTENT_DELETE_ACCESS_DENIED = new ErrorCode(1_053_130_073, "无权删除该知识内容"); + ErrorCode KNOWLEDGE_CONTENT_PERMISSION_MEMBERS_INVALID = new ErrorCode(1_053_130_074, "知识内容协作者数据无效"); + ErrorCode KNOWLEDGE_DOCUMENT_COMMENT_NOT_EXISTS = new ErrorCode(1_053_130_080, "知识库文档评论不存在"); + ErrorCode KNOWLEDGE_DOCUMENT_COMMENT_REPLY_INVALID = new ErrorCode(1_053_130_081, "回复的评论与文档不匹配"); + ErrorCode KNOWLEDGE_DOCUMENT_COMMENT_ACCESS_DENIED = new ErrorCode(1_053_130_082, "只能删除自己的评论"); + ErrorCode KNOWLEDGE_DOCUMENT_COMMENT_TYPE_INVALID = new ErrorCode(1_053_130_083, "只有富文本文档支持评论"); + ErrorCode KNOWLEDGE_LIBRARY_TEMPLATE_NOT_EXISTS = new ErrorCode(1_053_130_084, "知识库模板不存在"); + ErrorCode KNOWLEDGE_LIBRARY_TEMPLATE_INVALID = new ErrorCode(1_053_130_085, "知识库模板配置无效"); + ErrorCode KNOWLEDGE_LIBRARY_DIRECT_MEMBER_NOT_EXISTS = new ErrorCode(1_053_130_086, + "当前账号不是该知识库的直接成员"); + ErrorCode KNOWLEDGE_LIBRARY_TEMPLATE_NAME_DUPLICATE = new ErrorCode(1_053_130_087, "知识库模板名称已存在"); + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/MessageTemplateConstants.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/MessageTemplateConstants.java new file mode 100644 index 000000000..95c82fe0f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/MessageTemplateConstants.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.pms.enums; + +/** + * PMS 消息模板常量 + * + * @author 芋道源码 + */ +public interface MessageTemplateConstants { + + // ======================= 站内信模板 ======================= + + String WORK_ITEM_ASSIGNED = "pms_work_item_assigned"; // 工作项指派通知 + String KNOWLEDGE_DOCUMENT_SHARED = "pms_knowledge_document_shared"; // 知识文档分享通知 + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/PmsKnowledgeObjectTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/PmsKnowledgeObjectTypeEnum.java new file mode 100644 index 000000000..08ddf221a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/PmsKnowledgeObjectTypeEnum.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.pms.enums.kb; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 知识对象类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsKnowledgeObjectTypeEnum implements ArrayValuable { + + LIBRARY(1), // 知识库 + FOLDER(2), // 文件夹 + DOCUMENT(3), // 文档 + FILE(4); // 文件 + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsKnowledgeObjectTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsKnowledgeObjectTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeContentLevelEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeContentLevelEnum.java new file mode 100644 index 000000000..f1703d789 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeContentLevelEnum.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.pms.enums.kb.content; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 知识内容协作等级枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsKnowledgeContentLevelEnum implements ArrayValuable { + + MANAGE(1, "管理员"), + EDIT(2, "可编辑"), + PREVIEW(3, "仅预览"), + DOWNLOAD(4, "可下载"), + UPLOAD_DOWNLOAD(5, "可上传下载"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsKnowledgeContentLevelEnum::getLevel).toArray(Integer[]::new); + + /** + * 等级 + */ + private final Integer level; + /** + * 名称 + */ + private final String name; + + public static PmsKnowledgeContentLevelEnum valueOf(Integer level) { + return ArrayUtil.firstMatch(item -> item.getLevel().equals(level), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean canEdit(Integer level) { + return MANAGE.level.equals(level) || EDIT.level.equals(level) || UPLOAD_DOWNLOAD.level.equals(level); + } + + public static boolean canDelete(Integer level) { + return MANAGE.level.equals(level) || EDIT.level.equals(level); + } + + public static boolean canDownload(Integer level) { + return level != null && ObjectUtil.notEqual(PREVIEW.level, level); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeDocumentStatusEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeDocumentStatusEnum.java new file mode 100644 index 000000000..bde69c576 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeDocumentStatusEnum.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.enums.kb.content; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Set; + +/** + * PMS 知识库文档状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsKnowledgeDocumentStatusEnum implements ArrayValuable { + + RECYCLED(-1, "回收站"), + DRAFT(0, "草稿"), + NORMAL(1, "正常"), + TEMPLATE(2, "模板"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsKnowledgeDocumentStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 非回收站状态集合 + */ + public static final Set ACTIVE_STATUSES = SetUtils.asSet( + DRAFT.getStatus(), NORMAL.getStatus(), TEMPLATE.getStatus()); + + /** + * 状态 + */ + private final Integer status; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsKnowledgeDocumentStatusEnum valueOf(Integer status) { + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeDocumentTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeDocumentTypeEnum.java new file mode 100644 index 000000000..afdcca26e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/content/PmsKnowledgeDocumentTypeEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.enums.kb.content; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 知识库文档类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsKnowledgeDocumentTypeEnum implements ArrayValuable { + + RICH_TEXT(3, "富文本"), + FILE(4, "文件"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsKnowledgeDocumentTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsKnowledgeDocumentTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/library/PmsKnowledgeGroupTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/library/PmsKnowledgeGroupTypeEnum.java new file mode 100644 index 000000000..9db1bed3c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/library/PmsKnowledgeGroupTypeEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.kb.library; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 知识库分组类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsKnowledgeGroupTypeEnum implements ArrayValuable { + + ALL(1, "全部知识库"), + UNGROUPED(2, "未分组"), + CUSTOM(3, "自定义分组"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsKnowledgeGroupTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsKnowledgeGroupTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/library/PmsKnowledgeLibraryMemberLevelEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/library/PmsKnowledgeLibraryMemberLevelEnum.java new file mode 100644 index 000000000..2751e39f9 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/kb/library/PmsKnowledgeLibraryMemberLevelEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.kb.library; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 知识库成员等级枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsKnowledgeLibraryMemberLevelEnum implements ArrayValuable { + + CREATOR(1, "创建人"), + ADMIN(2, "管理员"), + MEMBER(3, "成员"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsKnowledgeLibraryMemberLevelEnum::getLevel).toArray(Integer[]::new); + + /** + * 等级 + */ + private final Integer level; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsKnowledgeLibraryMemberLevelEnum valueOf(Integer level) { + return ArrayUtil.firstMatch(item -> item.getLevel().equals(level), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/iteration/PmsIterationStatusEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/iteration/PmsIterationStatusEnum.java new file mode 100644 index 000000000..32ce03130 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/iteration/PmsIterationStatusEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.pm.iteration; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 迭代状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsIterationStatusEnum implements ArrayValuable { + + PLANNED(1, "未开始"), + ACTIVE(2, "进行中"), + COMPLETED(3, "已完成"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsIterationStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsIterationStatusEnum valueOf(Integer status) { + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectGroupTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectGroupTypeEnum.java new file mode 100644 index 000000000..d21abd8ac --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectGroupTypeEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.pm.project; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 项目分组类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsProjectGroupTypeEnum implements ArrayValuable { + + ALL(1, "全部项目"), + UNGROUPED(2, "未分组"), + CUSTOM(3, "自定义分组"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsProjectGroupTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsProjectGroupTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectLevelEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectLevelEnum.java new file mode 100644 index 000000000..6eaaaf4c6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectLevelEnum.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.pms.enums.pm.project; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 项目优先级枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsProjectLevelEnum implements ArrayValuable { + + HIGHEST(1, "最高"), + HIGH(2, "较高"), + NORMAL(3, "普通"), + LOW(4, "较低"), + LOWEST(5, "最低"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(PmsProjectLevelEnum::getLevel) + .toArray(Integer[]::new); + + /** + * 优先级 + */ + private final Integer level; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsProjectLevelEnum valueOf(Integer level) { + return ArrayUtil.firstMatch(item -> item.getLevel().equals(level), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectMemberLevelEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectMemberLevelEnum.java new file mode 100644 index 000000000..3f115287b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectMemberLevelEnum.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.pms.enums.pm.project; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.List; + +/** + * PMS 项目成员权限级别枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsProjectMemberLevelEnum implements ArrayValuable { + + OWNER(1, "项目拥有者"), + ADMIN(2, "项目管理员"), + WRITE(3, "编辑"), + READ(4, "只读"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsProjectMemberLevelEnum::getLevel).toArray(Integer[]::new); + + /** + * 可以管理项目的成员级别 + */ + public static final List MANAGER_LEVELS = Arrays.asList(OWNER.level, ADMIN.level); + /** + * 可以编辑项目业务数据的成员级别 + */ + public static final List WRITABLE_LEVELS = Arrays.asList(OWNER.level, ADMIN.level, WRITE.level); + + /** + * 权限级别 + */ + private final Integer level; + /** + * 权限级别名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean isOwner(Integer level) { + return ObjUtil.equal(OWNER.level, level); + } + + public static boolean isAdmin(Integer level) { + return ObjUtil.equal(ADMIN.level, level); + } + + public static boolean isManager(Integer level) { + return isOwner(level) || isAdmin(level); + } + + public static boolean isWritable(Integer level) { + return isManager(level) || ObjUtil.equal(WRITE.level, level); + } + + public static boolean isReadable(Integer level) { + return valueOf(level) != null; + } + + public static PmsProjectMemberLevelEnum valueOf(Integer level) { + return ArrayUtil.firstMatch(item -> ObjUtil.equal(item.level, level), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectSceneTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectSceneTypeEnum.java new file mode 100644 index 000000000..c1bc5dc5e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectSceneTypeEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.pm.project; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 项目列表场景枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsProjectSceneTypeEnum implements ArrayValuable { + + ALL(1, "全部项目"), + MANAGED(2, "我负责的"), + PARTICIPATED(3, "我参与的"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(PmsProjectSceneTypeEnum::getType) + .toArray(Integer[]::new); + + /** + * 场景类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsProjectSceneTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectSortTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectSortTypeEnum.java new file mode 100644 index 000000000..04a3101a2 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectSortTypeEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.enums.pm.project; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 项目排序类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsProjectSortTypeEnum implements ArrayValuable { + + ACCESS_TIME(1, "按访问时间"), + CREATE_TIME(2, "按创建时间"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(PmsProjectSortTypeEnum::getType) + .toArray(Integer[]::new); + + /** + * 排序类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsProjectSortTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectStatusEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectStatusEnum.java new file mode 100644 index 000000000..ae40e12e3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectStatusEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.pm.project; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 项目状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsProjectStatusEnum implements ArrayValuable { + + ACTIVE(1, "进行中"), + ARCHIVED(2, "已归档"), + RECYCLED(3, "回收站"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsProjectStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsProjectStatusEnum valueOf(Integer status) { + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectTypeEnum.java new file mode 100644 index 000000000..61af5beab --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/project/PmsProjectTypeEnum.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.enums.pm.project; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 项目类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsProjectTypeEnum implements ArrayValuable { + + GENERAL(1, "通用项目"), + AGILE(2, "敏捷开发项目"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(PmsProjectTypeEnum::getType) + .toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsProjectTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemActivityContentEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemActivityContentEnum.java new file mode 100644 index 000000000..79cb798f9 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemActivityContentEnum.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.pms.enums.pm.workitem; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * PMS 工作项动态内容枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsWorkItemActivityContentEnum { + + WORK_ITEM_CREATED("创建了工作项"), + WORK_ITEM_FIELD_UPDATED("将{}从「{}」修改为「{}」"), + WORK_ITEM_DESCRIPTION_UPDATED("更新了工作项描述"), + WORK_ITEM_LABELS_UPDATED("将标签从「{}」修改为「{}」"), + WORK_ITEM_MEMBERS_UPDATED("将参与人从「{}」修改为「{}」"), + WORK_ITEM_ATTACHMENTS_UPDATED("将附件从「{}」修改为「{}」"), + WORK_ITEM_PARENT_UPDATED("将父工作项从「{}」修改为「{}」"), + WORK_ITEM_NAME_UPDATED("将工作项名称从「{}」修改为「{}」"), + WORK_ITEM_STATUS_UPDATED("将状态从「{}」修改为「{}」"), + WORK_ITEM_MOVED_TO_BACKLOG("将工作项移回待规划"), + WORK_ITEM_PLANNED_TO_ITERATION("将工作项规划到迭代"), + WORK_ITEM_ARCHIVED("归档了工作项"), + WORK_ITEM_RECYCLED("将工作项移入回收站"), + WORK_ITEM_RESTORED("恢复了工作项"), + + COMMENT_CREATED("添加了评论"), + COMMENT_REPLIED("回复了评论"), + COMMENT_UPDATED("更新了评论"), + COMMENT_DELETED("删除了评论"), + + WORK_LOG_CREATED("登记了 {} 小时工时"), + WORK_LOG_UPDATED("更新了工时记录"); + + /** + * 动态内容模板 + */ + private final String content; + + /** + * 格式化动态内容 + * + * @param arguments 模板参数 + * @return 动态内容 + */ + public String format(Object... arguments) { + return StrUtil.format(content, arguments); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemDefectTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemDefectTypeEnum.java new file mode 100644 index 000000000..5436b1f3a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemDefectTypeEnum.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.pms.enums.pm.workitem; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 缺陷类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsWorkItemDefectTypeEnum implements ArrayValuable { + + FUNCTION(1, "功能问题"), + UI(2, "界面优化"), + USABILITY(3, "易用性问题"), + SECURITY(4, "安全问题"), + PERFORMANCE(5, "性能问题"), + CODE(6, "代码错误"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsWorkItemDefectTypeEnum::getType).toArray(Integer[]::new); + + /** + * 缺陷类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsWorkItemDefectTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemLifecycleStatusEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemLifecycleStatusEnum.java new file mode 100644 index 000000000..67f47ecd3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemLifecycleStatusEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.pm.workitem; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 工作项生命周期状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsWorkItemLifecycleStatusEnum implements ArrayValuable { + + ACTIVE(1, "正常"), + ARCHIVED(2, "已归档"), + RECYCLED(3, "回收站"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsWorkItemLifecycleStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsWorkItemLifecycleStatusEnum valueOf(Integer status) { + return ArrayUtil.firstMatch(item -> item.getStatus().equals(status), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemPriorityEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemPriorityEnum.java new file mode 100644 index 000000000..096d793d0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemPriorityEnum.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.pms.enums.pm.workitem; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 工作项优先级枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsWorkItemPriorityEnum implements ArrayValuable { + + NONE(0, "无"), + LOW(1, "低"), + MEDIUM(2, "中"), + HIGH(3, "高"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsWorkItemPriorityEnum::getPriority).toArray(Integer[]::new); + + /** + * 优先级 + */ + private final Integer priority; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsWorkItemPriorityEnum valueOf(Integer priority) { + return ArrayUtil.firstMatch(item -> item.getPriority().equals(priority), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemStatusTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemStatusTypeEnum.java new file mode 100644 index 000000000..6a799514c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemStatusTypeEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.pm.workitem; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 工作项语义状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsWorkItemStatusTypeEnum implements ArrayValuable { + + PENDING(1, "未开始"), + PROCESSING(2, "进行中"), + COMPLETED(3, "已完成"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsWorkItemStatusTypeEnum::getType).toArray(Integer[]::new); + + /** + * 状态类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsWorkItemStatusTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemTypeEnum.java b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemTypeEnum.java new file mode 100644 index 000000000..9e6d9c7ec --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-api/src/main/java/cn/iocoder/yudao/module/pms/enums/pm/workitem/PmsWorkItemTypeEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.pms.enums.pm.workitem; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * PMS 工作项类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PmsWorkItemTypeEnum implements ArrayValuable { + + REQUIREMENT(2, "需求"), + TASK(3, "任务"), + DEFECT(4, "缺陷"); + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(PmsWorkItemTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static PmsWorkItemTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/pom.xml b/yudao-module-pms/yudao-module-pms-server/pom.xml new file mode 100644 index 000000000..876eff6d7 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/pom.xml @@ -0,0 +1,125 @@ + + + + cn.iocoder.cloud + yudao-module-pms + ${revision} + + 4.0.0 + yudao-module-pms-server + + ${project.artifactId} + + pms 包下,项目管理(Project Management System)。 + 例如说:项目、工作事项、迭代、看板、工时和知识库等等 + + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-env + + + + + cn.iocoder.cloud + yudao-module-pms-api + ${revision} + + + cn.iocoder.cloud + yudao-module-system-api + ${revision} + + + cn.iocoder.cloud + yudao-module-infra-api + ${revision} + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-tenant + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-rpc + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-job + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-excel + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-monitor + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + + diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/PmsServerApplication.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/PmsServerApplication.java new file mode 100644 index 000000000..85a11e6f6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/PmsServerApplication.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.pms; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * PMS 项目的启动类 + * + * @author 芋道源码 + */ +@SpringBootApplication +public class PmsServerApplication { + + public static void main(String[] args) { + SpringApplication.run(PmsServerApplication.class, args); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeContentPermissionController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeContentPermissionController.java new file mode 100644 index 000000000..9cd6ac046 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeContentPermissionController.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.permission.PmsKnowledgeContentPermissionRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.permission.PmsKnowledgeContentPermissionUpdateReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionMemberDO; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.GetMapping; +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 java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识内容协作权限") +@RestController +@RequestMapping("/pms/kb/content-permission") +@Validated +public class PmsKnowledgeContentPermissionController { + + @Resource + private PmsKnowledgeContentPermissionService permissionService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @GetMapping("/get") + @Operation(summary = "获得知识内容协作权限") + @Parameter(name = "id", description = "协作权限编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getContentPermission(@RequestParam("id") Long id) { + Long userId = getLoginUserId(); + PmsKnowledgeContentPermissionDO permission = permissionService.getContentPermission(id, userId); + List members = permissionService.getContentPermissionMemberList(id, userId); + Integer currentUserLevel = permissionService.getCurrentUserContentPermissionLevel( + id, permission.getLibraryId(), userId); + return success(buildPermissionRespVO(permission, members, currentUserLevel)); + } + + @PutMapping("/update") + @Operation(summary = "更新知识内容协作权限") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateContentPermission(@Valid @RequestBody PmsKnowledgeContentPermissionUpdateReqVO updateReqVO) { + permissionService.updateContentPermission(updateReqVO, getLoginUserId()); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private PmsKnowledgeContentPermissionRespVO buildPermissionRespVO(PmsKnowledgeContentPermissionDO permission, + List members, + Integer currentUserLevel) { + Map userMap = adminUserApi.getUserMap( + convertSet(members, PmsKnowledgeContentPermissionMemberDO::getUserId)); + Map deptMap = deptApi.getDeptMap( + convertSet(members, PmsKnowledgeContentPermissionMemberDO::getDeptId)); + // 拼接 VO 返回 + PmsKnowledgeContentPermissionRespVO permissionVO = BeanUtils.toBean(permission, PmsKnowledgeContentPermissionRespVO.class) + .setCreatorUserId(NumberUtils.parseLong(permission.getCreator())) + .setCurrentUserLevel(currentUserLevel) + .setMembers(BeanUtils.toBean(members, PmsKnowledgeContentPermissionRespVO.Member.class)); + permissionVO.getMembers().forEach(member -> { + findAndThen(userMap, member.getUserId(), user -> member.setUserName(user.getNickname())); + findAndThen(deptMap, member.getDeptId(), dept -> member.setDeptName(dept.getName())); + }); + return permissionVO; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeDocumentController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeDocumentController.java new file mode 100644 index 000000000..aca906ca5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeDocumentController.java @@ -0,0 +1,202 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentSearchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentLikeDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeContentLevelEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentLikeService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库文档") +@RestController +@RequestMapping("/pms/kb/document") +@Validated +public class PmsKnowledgeDocumentController { + + @Resource + private PmsKnowledgeDocumentService documentService; + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + @Resource + private PmsKnowledgeFavoriteService favoriteService; + @Resource + private PmsKnowledgeDocumentLikeService documentLikeService; + @Resource + private PmsKnowledgeViewRecordService viewRecordService; + @Resource + private PmsKnowledgeLibraryService libraryService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建文档") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult createDocument(@Valid @RequestBody PmsKnowledgeDocumentCreateReqVO createReqVO) { + return success(documentService.createDocument(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新文档内容") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateDocument(@Valid @RequestBody PmsKnowledgeDocumentUpdateReqVO updateReqVO) { + documentService.updateDocument(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "将文档移入回收站") + @Parameter(name = "id", description = "文档编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult deleteDocument(@RequestParam("id") Long id) { + documentService.deleteDocument(id, getLoginUserId()); + return success(true); + } + + @PutMapping("/move") + @Operation(summary = "移动文档") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult moveDocument(@Valid @RequestBody PmsKnowledgeDocumentMoveReqVO moveReqVO) { + documentService.moveDocument(moveReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得文档详情") + @Parameter(name = "id", description = "文档编号", required = true, example = "1024") + @Parameter(name = "view", description = "是否记录本次浏览", example = "true") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getDocument( + @RequestParam("id") Long id, + @RequestParam(value = "view", defaultValue = "false") Boolean view) { + // 查询文档 + Long userId = getLoginUserId(); + PmsKnowledgeDocumentDO document = documentService.getDocument(id, userId); + // 记录浏览 + if (Boolean.TRUE.equals(view)) { + viewRecordService.createViewRecord(document.getLibraryId(), document.getType(), document.getId(), userId); + } + // 构建文档 VO + return success(buildDocumentRespVO(document, userId)); + } + + @GetMapping("/search-page") + @Operation(summary = "使用 MySQL 全局搜索文档") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getDocumentSearchPage( + @Valid PmsKnowledgeDocumentSearchPageReqVO pageReqVO) { + PageResult pageResult = documentService.getDocumentSearchPage( + pageReqVO, getLoginUserId()); + return success(new PageResult<>(buildDocumentRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + // ==================== 拼接 VO ==================== + + private PmsKnowledgeDocumentRespVO buildDocumentRespVO(PmsKnowledgeDocumentDO document, Long userId) { + // 1.1 拼接当前用户的内容权限和互动状态 + Integer currentUserLevel = contentPermissionService.getCurrentUserContentPermissionLevel( + document.getPermissionId(), document.getLibraryId(), userId); + Long creatorUserId = NumberUtils.parseLong(document.getCreator()); + PmsKnowledgeDocumentRespVO documentVO = BeanUtils.toBean(document, PmsKnowledgeDocumentRespVO.class) + .setCreatorUserId(creatorUserId).setCurrentUserLevel(currentUserLevel) + .setDownloadStatus(PmsKnowledgeContentLevelEnum.canDownload(currentUserLevel)) + .setPreviewUrl(document.getContent()) + .setFavoriteStatus(favoriteService.isFavorite(document.getType(), document.getId(), userId)); + // 1.2 文件不可下载时,隐藏实际文件地址 + if (PmsKnowledgeDocumentTypeEnum.FILE.getType().equals(document.getType()) + && Boolean.FALSE.equals(documentVO.getDownloadStatus())) { + documentVO.setContent(null); + } + + // 2. 一次查询并拼接创建人和点赞用户信息 + List likes = documentLikeService.getDocumentLikeList(document.getId()); + Set relatedUserIds = convertSet(likes, PmsKnowledgeDocumentLikeDO::getUserId); + relatedUserIds.add(creatorUserId); + Map userMap = adminUserApi.getUserMap(relatedUserIds); + findAndThen(userMap, creatorUserId, user -> documentVO.setCreatorUserName(user.getNickname())); + documentVO.setLikeStatus(CollUtil.anyMatch(likes, like -> userId.equals(like.getUserId()))) + .setLikeUsers(convertList(likes, like -> { + PmsKnowledgeDocumentRespVO.LikeUser likeUser = new PmsKnowledgeDocumentRespVO.LikeUser() + .setId(like.getUserId()); + findAndThen(userMap, like.getUserId(), user -> + likeUser.setNickname(user.getNickname()).setAvatar(user.getAvatar())); + return likeUser; + })); + return documentVO; + } + + private List buildDocumentRespVOList(List documents) { + if (CollUtil.isEmpty(documents)) { + return Collections.emptyList(); + } + Map libraryMap = libraryService.getLibraryMap( + convertSet(documents, PmsKnowledgeDocumentDO::getLibraryId)); + Map userMap = adminUserApi.getUserMap(convertSet(documents, + document -> NumberUtils.parseLong(document.getCreator()))); + return convertList(documents, document -> { + PmsKnowledgeDocumentRespVO documentVO = BeanUtils.toBean(document, PmsKnowledgeDocumentRespVO.class) + .setContent(null).setContentSummary(buildContentSummary(document.getContent())) + .setCreatorUserId(NumberUtils.parseLong(document.getCreator())); + findAndThen(libraryMap, document.getLibraryId(), + library -> documentVO.setLibraryName(library.getName())); + findAndThen(userMap, documentVO.getCreatorUserId(), + user -> documentVO.setCreatorUserName(user.getNickname())); + return documentVO; + }); + } + + /** + * 构造搜索结果摘要,避免直接返回完整正文 + */ + private String buildContentSummary(String content) { + if (content == null || content.trim().isEmpty()) { + return null; + } + String summary = content.replaceAll("<[^>]+>", " ").replaceAll("\\s+", " ").trim(); + return summary.length() > 120 ? summary.substring(0, 120) + "…" : summary; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeDocumentLabelController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeDocumentLabelController.java new file mode 100644 index 000000000..98452d826 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeDocumentLabelController.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentLabelService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库文档标签") +@RestController +@RequestMapping("/pms/kb/document-label") +@Validated +public class PmsKnowledgeDocumentLabelController { + + @Resource + private PmsKnowledgeDocumentLabelService documentLabelService; + @Resource + private PmsKnowledgeLibraryService libraryService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建文档标签") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult createDocumentLabel( + @Valid @RequestBody PmsKnowledgeDocumentLabelSaveReqVO saveReqVO) { + return success(documentLabelService.createDocumentLabel(saveReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改文档标签") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateDocumentLabel( + @Valid @RequestBody PmsKnowledgeDocumentLabelSaveReqVO saveReqVO) { + documentLabelService.updateDocumentLabel(saveReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除文档标签") + @Parameter(name = "id", description = "文档标签编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult deleteDocumentLabel(@RequestParam("id") Long id) { + documentLabelService.deleteDocumentLabel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得文档标签详情") + @Parameter(name = "id", description = "文档标签编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getDocumentLabel(@RequestParam("id") Long id) { + return success(BeanUtils.toBean(documentLabelService.getDocumentLabel(id), + PmsKnowledgeDocumentLabelRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得文档标签列表") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getDocumentLabelList() { + return success(BeanUtils.toBean(documentLabelService.getDocumentLabelList(), + PmsKnowledgeDocumentLabelRespVO.class)); + } + + @GetMapping("/document-page") + @Operation(summary = "获得指定标签的文档分页") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getDocumentPageByLabel( + @Valid PmsKnowledgeDocumentLabelPageReqVO pageReqVO) { + PageResult pageResult = documentLabelService.getDocumentPageByLabel( + pageReqVO, getLoginUserId()); + return success(new PageResult<>(buildDocumentRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + // ==================== 拼接 VO ==================== + + private List buildDocumentRespVOList(List documents) { + if (CollUtil.isEmpty(documents)) { + return Collections.emptyList(); + } + Map libraryMap = libraryService.getLibraryMap( + convertSet(documents, PmsKnowledgeDocumentDO::getLibraryId)); + Map userMap = adminUserApi.getUserMap(convertSet(documents, + document -> NumberUtils.parseLong(document.getCreator()))); + return convertList(documents, document -> { + PmsKnowledgeDocumentRespVO documentVO = BeanUtils.toBean(document, PmsKnowledgeDocumentRespVO.class) + .setContent(null).setCreatorUserId(NumberUtils.parseLong(document.getCreator())); + findAndThen(libraryMap, document.getLibraryId(), + library -> documentVO.setLibraryName(library.getName())); + findAndThen(userMap, documentVO.getCreatorUserId(), + user -> documentVO.setCreatorUserName(user.getNickname())); + return documentVO; + }); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeFolderController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeFolderController.java new file mode 100644 index 000000000..693f667b7 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/PmsKnowledgeFolderController.java @@ -0,0 +1,189 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeTreeRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeFolderService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库文件夹") +@RestController +@RequestMapping("/pms/kb/folder") +@Validated +public class PmsKnowledgeFolderController { + + @Resource + private PmsKnowledgeFolderService folderService; + @Resource + private PmsKnowledgeDocumentService documentService; + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + @Resource + private PmsKnowledgeFavoriteService favoriteService; + @Resource + private PmsKnowledgeViewRecordService viewRecordService; + + @PostMapping("/create") + @Operation(summary = "创建文件夹") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult createFolder(@Valid @RequestBody PmsKnowledgeFolderSaveReqVO saveReqVO) { + return success(folderService.createFolder(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新文件夹") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateFolder(@Valid @RequestBody PmsKnowledgeFolderSaveReqVO saveReqVO) { + folderService.updateFolder(saveReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "将文件夹移入回收站") + @Parameter(name = "id", description = "文件夹编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult deleteFolder(@RequestParam("id") Long id) { + folderService.deleteFolder(id, getLoginUserId()); + return success(true); + } + + @PutMapping("/move") + @Operation(summary = "移动文件夹") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult moveFolder(@Valid @RequestBody PmsKnowledgeFolderMoveReqVO moveReqVO) { + folderService.moveFolder(moveReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得文件夹详情") + @Parameter(name = "id", description = "文件夹编号", required = true, example = "1024") + @Parameter(name = "view", description = "是否记录本次浏览", example = "true") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getFolder( + @RequestParam("id") Long id, + @RequestParam(value = "view", defaultValue = "false") Boolean view) { + Long userId = getLoginUserId(); + PmsKnowledgeFolderDO folder = folderService.getFolder(id, userId); + if (Boolean.TRUE.equals(view)) { + viewRecordService.createViewRecord(folder.getLibraryId(), PmsKnowledgeObjectTypeEnum.FOLDER.getType(), + folder.getId(), userId); + } + return success(buildFolderRespVO(folder, userId)); + } + + @GetMapping("/tree") + @Operation(summary = "获得知识库目录树") + @Parameter(name = "libraryId", description = "知识库编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getFolderTree(@RequestParam("libraryId") Long libraryId) { + Long userId = getLoginUserId(); + List folders = folderService.getFolderList(libraryId, userId); + List documents = documentService.getDocumentList(libraryId, userId); + return success(buildTreeRespVO(libraryId, userId, + libraryMemberService.isLibraryWritable(libraryId, userId), + libraryMemberService.isLibraryAdmin(libraryId, userId), folders, documents)); + } + + // ==================== 拼接 VO ==================== + + private PmsKnowledgeFolderRespVO buildFolderRespVO(PmsKnowledgeFolderDO folder, Long userId) { + List folders = folderService.getFolderList(folder.getLibraryId(), userId); + List documents = documentService.getDocumentList(folder.getLibraryId(), userId); + int childFolderCount = CollUtil.count(folders, + item -> folder.getId().equals(item.getParentId())); + int documentCount = (int) CollUtil.count(documents, + item -> folder.getId().equals(item.getFolderId()) + && (item.getParentId() == null || item.getParentId() == 0L)); + return BeanUtils.toBean(folder, PmsKnowledgeFolderRespVO.class) + .setChildFolderCount(childFolderCount).setDocumentCount(documentCount) + .setCurrentUserLevel(contentPermissionService.getCurrentUserContentPermissionLevel(folder.getPermissionId(), folder.getLibraryId(), userId)) + .setFavoriteStatus(favoriteService.isFavorite(PmsKnowledgeObjectTypeEnum.FOLDER.getType(), folder.getId(), userId)); + } + + private PmsKnowledgeTreeRespVO buildTreeRespVO(Long libraryId, Long userId, boolean writeStatus, + boolean manageStatus, + List folders, List documents) { + // 1. 批量获得当前用户的内容权限等级 + Set permissionIds = convertSet(folders, PmsKnowledgeFolderDO::getPermissionId); + permissionIds.addAll(convertSet(documents, PmsKnowledgeDocumentDO::getPermissionId)); + Map levelMap = contentPermissionService.getCurrentUserContentPermissionLevelMap(permissionIds, libraryId, userId); + + // 2. 批量转换文件夹和文档节点 + Map folderNodeMap = convertMap(folders, + PmsKnowledgeFolderDO::getId, folder -> BeanUtils.toBean(folder, PmsKnowledgeTreeRespVO.FolderNode.class) + .setCurrentUserLevel(levelMap.get(folder.getPermissionId())).setChildren(new ArrayList<>()).setDocuments(new ArrayList<>())); + Map documentNodeMap = convertMap(documents, + PmsKnowledgeDocumentDO::getId, document -> BeanUtils.toBean(document, PmsKnowledgeTreeRespVO.DocumentNode.class) + .setCurrentUserLevel(levelMap.get(document.getPermissionId())).setChildren(new ArrayList<>())); + + // 3. 按父文档或所属文件夹组装文档树 + List rootDocuments = new ArrayList<>(); + for (PmsKnowledgeDocumentDO document : documents) { + PmsKnowledgeTreeRespVO.DocumentNode node = documentNodeMap.get(document.getId()); + PmsKnowledgeTreeRespVO.DocumentNode parentNode = documentNodeMap.get(document.getParentId()); + PmsKnowledgeTreeRespVO.FolderNode folderNode = folderNodeMap.get(document.getFolderId()); + if (parentNode != null) { + parentNode.getChildren().add(node); + } else if (folderNode != null) { + folderNode.getDocuments().add(node); + } else { + rootDocuments.add(node); + } + } + + // 4. 按父文件夹组装文件夹树 + List rootFolders = new ArrayList<>(); + for (PmsKnowledgeFolderDO folder : folders) { + PmsKnowledgeTreeRespVO.FolderNode node = folderNodeMap.get(folder.getId()); + PmsKnowledgeTreeRespVO.FolderNode parentNode = folderNodeMap.get(folder.getParentId()); + if (parentNode != null) { + parentNode.getChildren().add(node); + } else { + rootFolders.add(node); + } + } + return new PmsKnowledgeTreeRespVO().setLibraryId(libraryId).setWriteStatus(writeStatus) + .setManageStatus(manageStatus) + .setFolders(rootFolders).setDocuments(rootDocuments); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentCreateReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentCreateReqVO.java new file mode 100644 index 000000000..e4ef99b5d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentCreateReqVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库文档新增 Request VO") +@Data +public class PmsKnowledgeDocumentCreateReqVO { + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "知识库编号不能为空") + private Long libraryId; + + @Schema(description = "文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "文件夹编号不能为空") + private Long folderId; + + @Schema(description = "父文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "父文档编号不能为空") + private Long parentId; + + @Schema(description = "文档标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品说明") + @NotBlank(message = "文档标题不能为空") + @Size(max = 255, message = "文档标题不能超过 255 个字符") + private String title; + + @Schema(description = "文档类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "文档类型不能为空") + @InEnum(PmsKnowledgeDocumentTypeEnum.class) + private Integer type; + + @Schema(description = "文档内容或文件地址") + private String content; + + @Schema(description = "文件类型", example = "pdf") + @Size(max = 50, message = "文件类型不能超过 50 个字符") + private String fileType; + + @Schema(description = "文件大小,单位:字节", example = "102400") + @Min(value = 0, message = "文件大小不能小于 0") + @Max(value = 104857600, message = "文件大小不能超过 100 MB") + private Long fileSize; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentMoveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentMoveReqVO.java new file mode 100644 index 000000000..d5bc012ca --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentMoveReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库文档移动 Request VO") +@Data +public class PmsKnowledgeDocumentMoveReqVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "文档编号不能为空") + private Long id; + + @Schema(description = "目标知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "目标知识库编号不能为空") + private Long targetLibraryId; + + @Schema(description = "目标文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "目标文件夹编号不能为空") + private Long targetFolderId; + + @Schema(description = "目标父文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "目标父文档编号不能为空") + private Long targetParentId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentRespVO.java new file mode 100644 index 000000000..d07fe31b5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentRespVO.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库文档 Response VO") +@Data +public class PmsKnowledgeDocumentRespVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long libraryId; + + @Schema(description = "协作权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long permissionId; + + @Schema(description = "当前用户协作等级", example = "3") + private Integer currentUserLevel; + + @Schema(description = "当前用户是否可下载", example = "true") + private Boolean downloadStatus; + + @Schema(description = "知识库名称", example = "产品知识库") + private String libraryName; + + @Schema(description = "文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long folderId; + + @Schema(description = "父文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "文档标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品说明") + private String title; + + @Schema(description = "正文摘要") + private String contentSummary; + + @Schema(description = "文档内容或文件地址") + private String content; + + @Schema(description = "文件预览地址(下载权限不足时仍可用于在线预览)") + private String previewUrl; + + @Schema(description = "文档类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "文件类型", example = "pdf") + private String fileType; + + @Schema(description = "文件大小,单位:字节", example = "102400") + private Long fileSize; + + @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "标签编号列表", example = "[1024, 2048]") + private List labelIds; + + @Schema(description = "创建人用户编号", example = "1") + private Long creatorUserId; + + @Schema(description = "创建人姓名", example = "芋道") + private String creatorUserName; + + @Schema(description = "当前用户是否已关注") + private Boolean favoriteStatus; + + @Schema(description = "当前用户是否已点赞") + private Boolean likeStatus; + + @Schema(description = "点赞用户列表") + private List likeUsers; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "管理后台 - PMS 知识文档点赞用户 Response VO") + @Data + public static class LikeUser { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户昵称", example = "芋道") + private String nickname; + + @Schema(description = "用户头像") + private String avatar; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentSearchPageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentSearchPageReqVO.java new file mode 100644 index 000000000..07d9b8bbc --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentSearchPageReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - PMS 知识库文档全局搜索分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeDocumentSearchPageReqVO extends PageParam { + + @Schema(description = "搜索关键字", example = "产品方案") + private String keyword; + + @Schema(description = "知识库编号", example = "1024") + private Long libraryId; + + @Schema(description = "创建人用户编号", example = "1") + private Long creatorUserId; + + @Schema(description = "更新时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] updateTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentUpdateReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentUpdateReqVO.java new file mode 100644 index 000000000..7afdd5fa3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/document/PmsKnowledgeDocumentUpdateReqVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库文档修改 Request VO") +@Data +public class PmsKnowledgeDocumentUpdateReqVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "文档编号不能为空") + private Long id; + + @Schema(description = "文档标题", example = "产品说明") + @Size(max = 255, message = "文档标题不能超过 255 个字符") + private String title; + + @Schema(description = "文档内容或文件地址") + private String content; + + @Schema(description = "标签编号列表;传空集合表示清空标签") + private List labelIds; + + @Schema(description = "文件类型", example = "pdf") + @Size(max = 50, message = "文件类型不能超过 50 个字符") + private String fileType; + + @Schema(description = "文件大小,单位:字节", example = "102400") + @Min(value = 0, message = "文件大小不能小于 0") + @Max(value = 104857600, message = "文件大小不能超过 100 MB") + private Long fileSize; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderMoveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderMoveReqVO.java new file mode 100644 index 000000000..c30e02a87 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderMoveReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库文件夹移动 Request VO") +@Data +public class PmsKnowledgeFolderMoveReqVO { + + @Schema(description = "文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "文件夹编号不能为空") + private Long id; + + @Schema(description = "目标知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "目标知识库编号不能为空") + private Long targetLibraryId; + + @Schema(description = "目标父文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "目标父文件夹编号不能为空") + private Long targetParentId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderRespVO.java new file mode 100644 index 000000000..86bf50e83 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 知识库文件夹 Response VO") +@Data +public class PmsKnowledgeFolderRespVO { + + @Schema(description = "文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long libraryId; + + @Schema(description = "协作权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long permissionId; + + @Schema(description = "当前用户协作等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer currentUserLevel; + + @Schema(description = "父文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "文件夹标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品文档") + private String title; + + @Schema(description = "直属子文件夹数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer childFolderCount; + + @Schema(description = "直属文档数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + private Integer documentCount; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "当前用户是否已关注", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean favoriteStatus; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderSaveReqVO.java new file mode 100644 index 000000000..5e3e81957 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeFolderSaveReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库文件夹新增/修改 Request VO") +@Data +public class PmsKnowledgeFolderSaveReqVO { + + @Schema(description = "文件夹编号", example = "1024") + private Long id; + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "知识库编号不能为空") + private Long libraryId; + + @Schema(description = "父文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "父文件夹编号不能为空") + private Long parentId; + + @Schema(description = "文件夹标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品文档") + @NotBlank(message = "文件夹标题不能为空") + @Size(max = 255, message = "文件夹标题不能超过 255 个字符") + private String title; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeTreeRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeTreeRespVO.java new file mode 100644 index 000000000..9c3a9b207 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/folder/PmsKnowledgeTreeRespVO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库目录树 Response VO") +@Data +public class PmsKnowledgeTreeRespVO { + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long libraryId; + + @Schema(description = "当前用户是否可编辑", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean writeStatus; + + @Schema(description = "当前用户是否可管理目录结构", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean manageStatus; + + @Schema(description = "根文件夹列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List folders; + + @Schema(description = "根文档列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List documents; + + @Schema(description = "管理后台 - PMS 知识库文件夹树节点 Response VO") + @Data + public static class FolderNode { + + @Schema(description = "文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "协作权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long permissionId; + + @Schema(description = "当前用户协作等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer currentUserLevel; + + @Schema(description = "父文件夹编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "文件夹标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品文档") + private String title; + + @Schema(description = "子文件夹列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List children; + + @Schema(description = "文件夹下的文档列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List documents; + } + + @Schema(description = "管理后台 - PMS 知识库文档树节点 Response VO") + @Data + public static class DocumentNode { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "协作权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long permissionId; + + @Schema(description = "当前用户协作等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer currentUserLevel; + + @Schema(description = "父文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "文档标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品说明") + private String title; + + @Schema(description = "文档类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "文件类型", example = "pdf") + private String fileType; + + @Schema(description = "子文档列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List children; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelPageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelPageReqVO.java new file mode 100644 index 000000000..e675209cb --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelPageReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - PMS 按标签查询文档分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeDocumentLabelPageReqVO extends PageParam { + + @Schema(description = "文档标签编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "文档标签编号不能为空") + private Long labelId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelRespVO.java new file mode 100644 index 000000000..be6ddd1cd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 知识库文档标签 Response VO") +@Data +public class PmsKnowledgeDocumentLabelRespVO { + + @Schema(description = "文档标签编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品") + private String name; + + @Schema(description = "标签颜色", requiredMode = Schema.RequiredMode.REQUIRED, example = "#409EFF") + private String color; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelSaveReqVO.java new file mode 100644 index 000000000..1f77d0e68 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/label/PmsKnowledgeDocumentLabelSaveReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库文档标签新增/修改 Request VO") +@Data +public class PmsKnowledgeDocumentLabelSaveReqVO { + + @Schema(description = "文档标签编号", example = "1024") + private Long id; + + @Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品") + @NotBlank(message = "标签名称不能为空") + @Size(max = 255, message = "标签名称不能超过 255 个字符") + private String name; + + @Schema(description = "标签颜色", requiredMode = Schema.RequiredMode.REQUIRED, example = "#409EFF") + @NotBlank(message = "标签颜色不能为空") + @Size(max = 20, message = "标签颜色不能超过 20 个字符") + private String color; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/permission/PmsKnowledgeContentPermissionRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/permission/PmsKnowledgeContentPermissionRespVO.java new file mode 100644 index 000000000..c06b94cc4 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/permission/PmsKnowledgeContentPermissionRespVO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.permission; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识内容协作权限 Response VO") +@Data +public class PmsKnowledgeContentPermissionRespVO { + + @Schema(description = "协作权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long libraryId; + + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean openStatus; + + @Schema(description = "公开协作等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer openLevel; + + @Schema(description = "创建人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long creatorUserId; + + @Schema(description = "当前用户协作等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer currentUserLevel; + + @Schema(description = "协作者列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List members; + + @Schema(description = "管理后台 - PMS 知识内容协作者 Response VO") + @Data + public static class Member { + + @Schema(description = "协作者编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户编号", example = "1") + private Long userId; + + @Schema(description = "用户昵称", example = "芋道") + private String userName; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门名称", example = "研发部") + private String deptName; + + @Schema(description = "协作等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer level; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/permission/PmsKnowledgeContentPermissionUpdateReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/permission/PmsKnowledgeContentPermissionUpdateReqVO.java new file mode 100644 index 000000000..3ccedbdf3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/content/vo/permission/PmsKnowledgeContentPermissionUpdateReqVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.permission; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeContentLevelEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 更新知识内容协作权限 Request VO") +@Data +public class PmsKnowledgeContentPermissionUpdateReqVO { + + @Schema(description = "协作权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "协作权限编号不能为空") + private Long id; + + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否公开不能为空") + private Boolean openStatus; + + @Schema(description = "公开协作等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "公开协作等级不能为空") + @InEnum(PmsKnowledgeContentLevelEnum.class) + private Integer openLevel; + + @Schema(description = "协作者列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "协作者列表不能为空") + private List members; + + @Schema(description = "管理后台 - PMS 知识内容协作者保存项") + @Data + public static class Member { + + @Schema(description = "用户编号", example = "1") + private Long userId; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "协作等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "协作等级不能为空") + @InEnum(PmsKnowledgeContentLevelEnum.class) + private Integer level; + + @AssertTrue(message = "协作者必须且只能选择用户或部门") + @JsonIgnore + public boolean isUserOrDeptValid() { + return (userId != null && deptId == null) || (userId == null && deptId != null); + } + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentCommentController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentCommentController.java new file mode 100644 index 000000000..235e07cf7 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentCommentController.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.comment.PmsKnowledgeDocumentCommentRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.comment.PmsKnowledgeDocumentCommentSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentCommentDO; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentCommentService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库文档评论") +@RestController +@RequestMapping("/pms/kb/document-comment") +@Validated +public class PmsKnowledgeDocumentCommentController { + + @Resource + private PmsKnowledgeDocumentCommentService commentService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建知识库文档评论") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult createDocumentComment(@Valid @RequestBody PmsKnowledgeDocumentCommentSaveReqVO saveReqVO) { + return success(commentService.createDocumentComment(saveReqVO, getLoginUserId())); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除知识库文档评论") + @Parameter(name = "id", description = "评论编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult deleteDocumentComment(@RequestParam("id") Long id) { + commentService.deleteDocumentComment(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得知识库文档评论列表") + @Parameter(name = "documentId", description = "文档编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getDocumentCommentList( + @RequestParam("documentId") Long documentId) { + List comments = commentService.getDocumentCommentList(documentId, getLoginUserId()); + return success(buildCommentRespVOList(comments)); + } + + // ==================== 拼接 VO ==================== + + private List buildCommentRespVOList( + List comments) { + if (CollUtil.isEmpty(comments)) { + return Collections.emptyList(); + } + // 1. 批量查询评论人和回复对象 + Map userMap = adminUserApi.getUserMap(convertSetByFlatMap(comments, + comment -> Stream.of(comment.getUserId(), comment.getReplyUserId()))); + + // 2. 批量转换评论,避免回复组装依赖查询顺序 + Map commentMap = convertMap(comments, + PmsKnowledgeDocumentCommentDO::getId, comment -> { + PmsKnowledgeDocumentCommentRespVO commentVO = BeanUtils.toBean(comment, + PmsKnowledgeDocumentCommentRespVO.class).setChildren(new ArrayList<>()); + findAndThen(userMap, comment.getUserId(), user -> commentVO.setUserName(user.getNickname())); + findAndThen(userMap, comment.getReplyUserId(), + user -> commentVO.setReplyUserName(user.getNickname())); + return commentVO; + }); + + // 3. 按主评论组织回复列表 + List result = new ArrayList<>(); + for (PmsKnowledgeDocumentCommentDO comment : comments) { + PmsKnowledgeDocumentCommentRespVO commentVO = commentMap.get(comment.getId()); + if (PmsKnowledgeDocumentCommentDO.MAIN_ID_ROOT.equals(comment.getMainId())) { + result.add(commentVO); + continue; + } + findAndThen(commentMap, comment.getMainId(), mainComment -> mainComment.getChildren().add(commentVO)); + } + return result; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentLikeController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentLikeController.java new file mode 100644 index 000000000..98cd56568 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentLikeController.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentLikeService; +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 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.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库文档点赞") +@RestController +@RequestMapping("/pms/kb/document-like") +@Validated +public class PmsKnowledgeDocumentLikeController { + + @Resource + private PmsKnowledgeDocumentLikeService documentLikeService; + + @PostMapping("/create") + @Operation(summary = "点赞文档") + @Parameter(name = "documentId", description = "文档编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult createDocumentLike(@RequestParam("documentId") Long documentId) { + documentLikeService.createDocumentLike(documentId, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "取消点赞文档") + @Parameter(name = "documentId", description = "文档编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult deleteDocumentLike(@RequestParam("documentId") Long documentId) { + documentLikeService.deleteDocumentLike(documentId, getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentShareController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentShareController.java new file mode 100644 index 000000000..361c81d29 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentShareController.java @@ -0,0 +1,101 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share.PmsKnowledgeDocumentSharePublicRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share.PmsKnowledgeDocumentShareRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share.PmsKnowledgeDocumentShareSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share.PmsKnowledgeDocumentShareUpdateMemberListReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentShareDO; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentShareService; +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.annotation.security.PermitAll; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库文档分享") +@RestController +@RequestMapping("/pms/kb/document-share") +@Validated +public class PmsKnowledgeDocumentShareController { + + @Resource + private PmsKnowledgeDocumentShareService documentShareService; + + @Resource + private FileApi fileApi; + + @PostMapping("/open") + @Operation(summary = "开启文档分享") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult openShare( + @Valid @RequestBody PmsKnowledgeDocumentShareSaveReqVO reqVO) { + PmsKnowledgeDocumentShareDO share = documentShareService.openShare(reqVO.getDocumentId(), + reqVO.getShareUserIds(), getLoginUserId()); + return success(BeanUtils.toBean(share, PmsKnowledgeDocumentShareRespVO.class)); + } + + @PutMapping("/update-member-list") + @Operation(summary = "更新文档内部分享成员") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateShareMemberList( + @Valid @RequestBody PmsKnowledgeDocumentShareUpdateMemberListReqVO reqVO) { + documentShareService.updateShareMemberList(reqVO.getDocumentId(), reqVO.getShareUserIds(), getLoginUserId()); + return success(true); + } + + @PutMapping("/close") + @Operation(summary = "关闭文档分享") + @Parameter(name = "documentId", description = "文档编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult closeShare(@RequestParam("documentId") Long documentId) { + documentShareService.closeShare(documentId, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得文档分享配置") + @Parameter(name = "documentId", description = "文档编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getDocumentShare(@RequestParam("documentId") Long documentId) { + PmsKnowledgeDocumentShareDO share = documentShareService.getActiveDocumentShare(documentId, getLoginUserId()); + return success(BeanUtils.toBean(share, PmsKnowledgeDocumentShareRespVO.class)); + } + + @GetMapping("/get-by-token") + @Operation(summary = "通过分享令牌获得文档") + @Parameter(name = "token", description = "分享令牌", required = true) + @PermitAll + @TenantIgnore + public CommonResult getDocumentByToken( + @RequestParam("token") @NotBlank(message = "分享令牌不能为空") String token) { + PmsKnowledgeDocumentDO document = documentShareService.getDocumentByShareToken(token); + PmsKnowledgeDocumentSharePublicRespVO documentVO = BeanUtils.toBean( + document, PmsKnowledgeDocumentSharePublicRespVO.class); + if (PmsKnowledgeDocumentTypeEnum.FILE.getType().equals(document.getType())) { + documentVO.setContent(null).setPreviewUrl( + fileApi.presignGetUrl(document.getContent(), null).getCheckedData()); + } + return success(documentVO); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeFavoriteController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeFavoriteController.java new file mode 100644 index 000000000..0439bb2aa --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeFavoriteController.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoritePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoriteSaveReqVO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识关注") +@RestController +@RequestMapping("/pms/kb/favorite") +@Validated +public class PmsKnowledgeFavoriteController { + + @Resource + private PmsKnowledgeFavoriteService favoriteService; + + @PostMapping("/create") + @Operation(summary = "关注知识对象") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult createFavorite(@Valid @RequestBody PmsKnowledgeFavoriteSaveReqVO saveReqVO) { + favoriteService.createFavorite(saveReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "取消关注知识对象") + @Parameter(name = "type", description = "对象类型", required = true, example = "3") + @Parameter(name = "entityId", description = "对象编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult deleteFavorite( + @RequestParam("type") @InEnum(PmsKnowledgeObjectTypeEnum.class) Integer type, + @RequestParam("entityId") Long entityId) { + favoriteService.deleteFavorite(type, entityId, getLoginUserId()); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得我的关注分页") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getFavoritePage( + @Valid PmsKnowledgeFavoritePageReqVO pageReqVO) { + return success(favoriteService.getFavoritePage(pageReqVO, getLoginUserId())); + } + + @GetMapping("/list") + @Operation(summary = "获得知识库内我的关注列表") + @Parameter(name = "libraryId", description = "知识库编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getFavoriteListByLibraryId( + @RequestParam("libraryId") Long libraryId) { + return success(favoriteService.getFavoriteListByLibraryId(libraryId, getLoginUserId())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeViewRecordController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeViewRecordController.java new file mode 100644 index 000000000..16fc9b3ac --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeViewRecordController.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.viewrecord.PmsKnowledgeRecentListRespVO; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识最近浏览") +@RestController +@RequestMapping("/pms/kb/view-record") +@Validated +public class PmsKnowledgeViewRecordController { + + @Resource + private PmsKnowledgeViewRecordService viewRecordService; + + @GetMapping("/recent-list") + @Operation(summary = "获得最近浏览列表") + @Parameter(name = "libraryId", description = "知识库编号;不传时查询全部可读知识库", example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getRecentViewRecordList( + @RequestParam(value = "libraryId", required = false) Long libraryId) { + return success(viewRecordService.getRecentViewRecordList(libraryId, getLoginUserId())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/PmsKnowledgeInteractionItemRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/PmsKnowledgeInteractionItemRespVO.java new file mode 100644 index 000000000..ac61ad402 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/PmsKnowledgeInteractionItemRespVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 知识交互对象 Response VO") +@Data +public class PmsKnowledgeInteractionItemRespVO { + + @Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "对象编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long entityId; + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long libraryId; + + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品知识库") + private String libraryName; + + @Schema(description = "对象名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品说明") + private String name; + + @Schema(description = "知识库简介") + private String description; + + @Schema(description = "文件夹编号", example = "1024") + private Long folderId; + + @Schema(description = "文档编号", example = "1024") + private Long documentId; + + @Schema(description = "文件类型", example = "pdf") + private String fileType; + + @Schema(description = "文件大小,单位:字节") + private Long fileSize; + + @Schema(description = "对象更新时间") + private LocalDateTime targetUpdateTime; + + @Schema(description = "记录时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/comment/PmsKnowledgeDocumentCommentRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/comment/PmsKnowledgeDocumentCommentRespVO.java new file mode 100644 index 000000000..fb0c4f6e4 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/comment/PmsKnowledgeDocumentCommentRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.comment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库文档评论 Response VO") +@Data +public class PmsKnowledgeDocumentCommentRespVO { + + @Schema(description = "评论编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long documentId; + + @Schema(description = "评论人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long userId; + + @Schema(description = "评论人昵称", example = "芋道") + private String userName; + + @Schema(description = "主评论编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Long mainId; + + @Schema(description = "回复对象用户编号", example = "2") + private Long replyUserId; + + @Schema(description = "回复对象昵称", example = "源码") + private String replyUserName; + + @Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "回复列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List children; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/comment/PmsKnowledgeDocumentCommentSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/comment/PmsKnowledgeDocumentCommentSaveReqVO.java new file mode 100644 index 000000000..efbed8db2 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/comment/PmsKnowledgeDocumentCommentSaveReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.comment; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库文档评论保存 Request VO") +@Data +public class PmsKnowledgeDocumentCommentSaveReqVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "文档编号不能为空") + private Long documentId; + + @Schema(description = "主评论编号", example = "0") + private Long mainId; + + @Schema(description = "回复对象用户编号", example = "1") + private Long replyUserId; + + @Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "文档内容很清楚") + @NotBlank(message = "评论内容不能为空") + @Size(max = 2000, message = "评论内容不能超过 2000 个字符") + private String content; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/favorite/PmsKnowledgeFavoritePageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/favorite/PmsKnowledgeFavoritePageReqVO.java new file mode 100644 index 000000000..a8bdf557b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/favorite/PmsKnowledgeFavoritePageReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - PMS 知识收藏(关注)分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PmsKnowledgeFavoritePageReqVO extends PageParam { + + @Schema(description = "对象类型", example = "3") + @InEnum(value = PmsKnowledgeObjectTypeEnum.class, message = "对象类型必须是 {value}") + private Integer type; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/favorite/PmsKnowledgeFavoriteSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/favorite/PmsKnowledgeFavoriteSaveReqVO.java new file mode 100644 index 000000000..e77b3f251 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/favorite/PmsKnowledgeFavoriteSaveReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识收藏(关注)保存 Request VO") +@Data +public class PmsKnowledgeFavoriteSaveReqVO { + + @Schema(description = "对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "对象类型不能为空") + @InEnum(value = PmsKnowledgeObjectTypeEnum.class, message = "对象类型必须是 {value}") + private Integer type; + + @Schema(description = "对象编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "对象编号不能为空") + private Long entityId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentSharePublicRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentSharePublicRespVO.java new file mode 100644 index 000000000..f93f2cf05 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentSharePublicRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "PMS 知识库文档公开分享 Response VO") +@Data +public class PmsKnowledgeDocumentSharePublicRespVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "文档标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品说明") + private String title; + + @Schema(description = "文档内容或文件地址") + private String content; + + @Schema(description = "文件临时预览地址") + private String previewUrl; + + @Schema(description = "文档类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "文件类型", example = "pdf") + private String fileType; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareRespVO.java new file mode 100644 index 000000000..3c9fc5198 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库文档分享 Response VO") +@Data +public class PmsKnowledgeDocumentShareRespVO { + + @Schema(description = "分享编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long documentId; + + @Schema(description = "内部分享成员用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List shareUserIds; + + @Schema(description = "外部查看令牌", requiredMode = Schema.RequiredMode.REQUIRED) + private String token; + + @Schema(description = "分享状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "关闭人用户编号") + private Long closeUserId; + + @Schema(description = "关闭时间") + private LocalDateTime closeTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareSaveReqVO.java new file mode 100644 index 000000000..886e4a622 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareSaveReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库文档分享保存 Request VO") +@Data +public class PmsKnowledgeDocumentShareSaveReqVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "文档编号不能为空") + private Long documentId; + + @Schema(description = "内部分享成员用户编号列表") + private List<@NotNull(message = "分享成员用户编号不能为空") Long> shareUserIds; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareUpdateMemberListReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareUpdateMemberListReqVO.java new file mode 100644 index 000000000..36f503568 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/share/PmsKnowledgeDocumentShareUpdateMemberListReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库文档分享成员列表更新 Request VO") +@Data +public class PmsKnowledgeDocumentShareUpdateMemberListReqVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "文档编号不能为空") + private Long documentId; + + @Schema(description = "内部分享成员用户编号列表") + private List<@NotNull(message = "分享成员用户编号不能为空") Long> shareUserIds; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/viewrecord/PmsKnowledgeRecentListRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/viewrecord/PmsKnowledgeRecentListRespVO.java new file mode 100644 index 000000000..b0c539fea --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/vo/viewrecord/PmsKnowledgeRecentListRespVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.viewrecord; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识最近浏览 Response VO") +@Data +public class PmsKnowledgeRecentListRespVO { + + @Schema(description = "今天浏览列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List todayItems; + + @Schema(description = "昨天浏览列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List yesterdayItems; + + @Schema(description = "更早 30 天浏览列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List recent30DayItems; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeGroupController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeGroupController.java new file mode 100644 index 000000000..c3b6424f0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeGroupController.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeGroupRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeGroupSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeGroupSortReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeLibraryMoveGroupReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeGroupDO; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeGroupService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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 java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库分组") +@RestController +@RequestMapping("/pms/kb/group") +@Validated +public class PmsKnowledgeGroupController { + + @Resource + private PmsKnowledgeGroupService knowledgeGroupService; + + @PostMapping("/create") + @Operation(summary = "创建知识库分组") + @PreAuthorize("@ss.hasPermission('pms:kb:library:create')") + public CommonResult createGroup(@Valid @RequestBody PmsKnowledgeGroupSaveReqVO saveReqVO) { + return success(knowledgeGroupService.createGroup(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改知识库分组") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateGroup(@Valid @RequestBody PmsKnowledgeGroupSaveReqVO saveReqVO) { + knowledgeGroupService.updateGroup(saveReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-sort") + @Operation(summary = "修改知识库分组排序") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateGroupSort( + @Valid @RequestBody PmsKnowledgeGroupSortReqVO sortReqVO) { + knowledgeGroupService.updateGroupSort(sortReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除知识库分组") + @Parameter(name = "id", description = "知识库分组编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult deleteGroup(@RequestParam("id") Long id) { + knowledgeGroupService.deleteGroup(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得知识库分组列表") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getGroupList() { + Long userId = getLoginUserId(); + List groups = knowledgeGroupService.getGroupList(userId); + Map groupCountMap = knowledgeGroupService.getGroupLibraryCountMap(userId, + convertSet(groups, PmsKnowledgeGroupDO::getId)); + return success(convertList(groups, group -> BeanUtils.toBean(group, PmsKnowledgeGroupRespVO.class) + .setLibraryCount(groupCountMap.getOrDefault(group.getId(), 0)))); + } + + @GetMapping("/get") + @Operation(summary = "获得知识库分组") + @Parameter(name = "id", description = "知识库分组编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getGroup(@RequestParam("id") Long id) { + return success(BeanUtils.toBean(knowledgeGroupService.getGroup(id, getLoginUserId()), + PmsKnowledgeGroupRespVO.class)); + } + + @PutMapping("/move") + @Operation(summary = "移动知识库到个人分组") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult moveLibraryToGroup( + @Valid @RequestBody PmsKnowledgeLibraryMoveGroupReqVO moveReqVO) { + knowledgeGroupService.moveLibraryToGroup(moveReqVO, getLoginUserId()); + return success(true); + } +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryController.java new file mode 100644 index 000000000..2ef286af2 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryController.java @@ -0,0 +1,159 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibraryPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibraryRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibrarySaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库") +@RestController +@RequestMapping("/pms/kb/library") +@Validated +public class PmsKnowledgeLibraryController { + + @Resource + private PmsKnowledgeLibraryService libraryService; + @Resource + private PmsKnowledgeLibraryMemberService memberService; + @Resource + private PmsKnowledgeDocumentService documentService; + @Resource + private PmsKnowledgeFavoriteService favoriteService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private PermissionApi permissionApi; + + @PostMapping("/create") + @Operation(summary = "创建知识库") + @PreAuthorize("@ss.hasPermission('pms:kb:library:create')") + public CommonResult createLibrary(@Valid @RequestBody PmsKnowledgeLibrarySaveReqVO saveReqVO) { + return success(libraryService.createLibrary(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新知识库") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateLibrary(@Valid @RequestBody PmsKnowledgeLibrarySaveReqVO saveReqVO) { + libraryService.updateLibrary(saveReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除知识库") + @Parameter(name = "id", description = "知识库编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult deleteLibrary(@RequestParam("id") Long id) { + libraryService.deleteLibrary(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得知识库详情") + @Parameter(name = "id", description = "知识库编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult getLibrary(@RequestParam("id") Long id) { + Long userId = getLoginUserId(); + PmsKnowledgeLibraryDO library = memberService.validateLibraryReadable(id, userId); + return success(CollUtil.getFirst(buildLibraryRespVOList(Collections.singletonList(library), userId))); + } + + @GetMapping("/page") + @Operation(summary = "获得知识库分页") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getLibraryPage( + @Valid PmsKnowledgeLibraryPageReqVO pageReqVO) { + Long userId = getLoginUserId(); + PageResult pageResult = libraryService.getLibraryPage(pageReqVO, userId); + return success(new PageResult<>(buildLibraryRespVOList(pageResult.getList(), userId), pageResult.getTotal())); + } + + // ==================== 拼接 VO ==================== + + private List buildLibraryRespVOList( + List libraries, Long userId) { + if (CollUtil.isEmpty(libraries)) { + return Collections.emptyList(); + } + + // 1. 批量查询知识库成员、内容数量、创建人和当前用户权限信息 + Set libraryIds = convertSet(libraries, PmsKnowledgeLibraryDO::getId); + Map> memberListMap = memberService.getLibraryMemberListMap(libraryIds); + Map> documentTypeCountMap = documentService.getDocumentTypeCountMap(libraryIds); + Set userIds = convertSet(libraries, library -> NumberUtils.parseLong(library.getCreator())); + Map userMap = adminUserApi.getUserMap(userIds); + AdminUserRespDTO currentUser = adminUserApi.getUser(userId).getCheckedData(); + Long deptId = currentUser != null ? currentUser.getDeptId() : null; + boolean superAdmin = permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData(); + Set favoriteLibraryIds = favoriteService.getFavoriteEntityIdSet( + PmsKnowledgeObjectTypeEnum.LIBRARY.getType(), libraryIds, userId); + + // 2. 拼接每个知识库的成员权限、统计数量、收藏状态和创建人名称 + return convertList(libraries, library -> { + List members = memberListMap.getOrDefault(library.getId(), Collections.emptyList()); + PmsKnowledgeLibraryMemberDO directMember = CollUtil.findOne(members, member -> userId.equals(member.getUserId())); + PmsKnowledgeLibraryMemberDO currentMember = directMember; + if (currentMember == null && deptId != null) { + currentMember = CollUtil.findOne(members, member -> deptId.equals(member.getDeptId())); + } + boolean adminStatus = superAdmin || currentMember != null + && (PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel().equals(currentMember.getLevel()) + || PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel().equals(currentMember.getLevel())); + PmsKnowledgeLibraryRespVO libraryVO = BeanUtils.toBean(library, PmsKnowledgeLibraryRespVO.class) + .setCreatorUserId(NumberUtils.parseLong(library.getCreator())).setMemberCount(members.size()) + .setWriteStatus(superAdmin || currentMember != null).setAdminStatus(adminStatus) + .setExitStatus(directMember != null && !PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel().equals(directMember.getLevel())) + .setFavoriteStatus(favoriteLibraryIds.contains(library.getId())); + Map typeCountMap = documentTypeCountMap.getOrDefault(library.getId(), Collections.emptyMap()); + libraryVO.setDocumentCount(typeCountMap.getOrDefault(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType(), 0L)) + .setFileCount(typeCountMap.getOrDefault(PmsKnowledgeDocumentTypeEnum.FILE.getType(), 0L)); + findAndThen(userMap, libraryVO.getCreatorUserId(), user -> libraryVO.setCreatorUserName(user.getNickname())); + return libraryVO; + }); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryMemberController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryMemberController.java new file mode 100644 index 000000000..8cf710a73 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryMemberController.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.member.PmsKnowledgeLibraryMemberRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.member.PmsKnowledgeLibraryUpdateMemberListReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库成员") +@RestController +@RequestMapping("/pms/kb/library-member") +@Validated +public class PmsKnowledgeLibraryMemberController { + + @Resource + private PmsKnowledgeLibraryMemberService memberService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @GetMapping("/list") + @Operation(summary = "获得知识库成员列表") + @Parameter(name = "libraryId", description = "知识库编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult> getLibraryMemberList(@RequestParam("libraryId") Long libraryId) { + List members = memberService.getLibraryMemberList(libraryId, getLoginUserId()); + return success(buildLibraryMemberRespVOList(members)); + } + + @PutMapping("/update-list") + @Operation(summary = "更新知识库成员") + @PreAuthorize("@ss.hasPermission('pms:kb:library:update')") + public CommonResult updateLibraryMemberList(@Valid @RequestBody PmsKnowledgeLibraryUpdateMemberListReqVO updateReqVO) { + memberService.updateLibraryMemberList(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/exit") + @Operation(summary = "主动退出知识库") + @Parameter(name = "libraryId", description = "知识库编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:query')") + public CommonResult exitLibrary(@RequestParam("libraryId") Long libraryId) { + memberService.exitLibrary(libraryId, getLoginUserId()); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private List buildLibraryMemberRespVOList( + List members) { + if (CollUtil.isEmpty(members)) { + return Collections.emptyList(); + } + + // 1. 批量查询用户和部门 + Map userMap = adminUserApi.getUserMap( + convertSet(members, PmsKnowledgeLibraryMemberDO::getUserId)); + Map deptMap = deptApi.getDeptMap( + convertSet(members, PmsKnowledgeLibraryMemberDO::getDeptId)); + Map parentDeptMap = deptApi.getDeptMap(convertSet(deptMap.values(), DeptRespDTO::getParentId)); + // 2. 转换成员响应,并拼接用户和部门名称 + return convertList(members, member -> { + PmsKnowledgeLibraryMemberRespVO memberVO = BeanUtils.toBean(member, PmsKnowledgeLibraryMemberRespVO.class); + findAndThen(userMap, member.getUserId(), user -> + memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar())); + findAndThen(deptMap, member.getDeptId(), dept -> { + memberVO.setDeptName(dept.getName()).setParentDeptId(dept.getParentId()); + findAndThen(parentDeptMap, dept.getParentId(), parent -> memberVO.setParentDeptName(parent.getName())); + }); + return memberVO; + }); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryTemplateController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryTemplateController.java new file mode 100644 index 000000000..cf023f5e8 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/PmsKnowledgeLibraryTemplateController.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplatePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplateRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplateSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryTemplateDO; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryTemplateService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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 cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 管理后台 - PMS 知识库模板 Controller + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - PMS 知识库模板") +@RestController +@RequestMapping("/pms/kb/library-template") +@Validated +public class PmsKnowledgeLibraryTemplateController { + + @Resource + private PmsKnowledgeLibraryTemplateService libraryTemplateService; + + @PostMapping("/create") + @Operation(summary = "创建知识库模板") + @PreAuthorize("@ss.hasPermission('pms:kb:library-template:create')") + public CommonResult createLibraryTemplate( + @Valid @RequestBody PmsKnowledgeLibraryTemplateSaveReqVO createReqVO) { + return success(libraryTemplateService.createLibraryTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新知识库模板") + @PreAuthorize("@ss.hasPermission('pms:kb:library-template:update')") + public CommonResult updateLibraryTemplate( + @Valid @RequestBody PmsKnowledgeLibraryTemplateSaveReqVO updateReqVO) { + libraryTemplateService.updateLibraryTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除知识库模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library-template:delete')") + public CommonResult deleteLibraryTemplate(@RequestParam("id") Long id) { + libraryTemplateService.deleteLibraryTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得知识库模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library-template:query')") + public CommonResult getLibraryTemplate(@RequestParam("id") Long id) { + PmsKnowledgeLibraryTemplateDO template = libraryTemplateService.getLibraryTemplate(id); + return success(BeanUtils.toBean(template, PmsKnowledgeLibraryTemplateRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取知识库模板全列表", description = "只包含开启状态模板,返回模板选择所需的基本信息和文档标题") + public CommonResult> getSimpleLibraryTemplateList() { + return success(convertList(libraryTemplateService.getLibraryTemplateList(CommonStatusEnum.ENABLE.getStatus()), + template -> new PmsKnowledgeLibraryTemplateRespVO().setId(template.getId()) + .setName(template.getName()).setCoverUrl(template.getCoverUrl()).setDescription(template.getDescription()) + .setDocuments(convertList(template.getDocuments(), document -> + new PmsKnowledgeLibraryTemplateSaveReqVO.Document().setTitle(document.getTitle()))))); + } + + @GetMapping("/page") + @Operation(summary = "获得知识库模板分页") + @PreAuthorize("@ss.hasPermission('pms:kb:library-template:query')") + public CommonResult> getLibraryTemplatePage( + @Valid PmsKnowledgeLibraryTemplatePageReqVO pageReqVO) { + PageResult pageResult = libraryTemplateService + .getLibraryTemplatePage(pageReqVO); + return success(new PageResult<>(BeanUtils.toBean(pageResult.getList(), PmsKnowledgeLibraryTemplateRespVO.class), + pageResult.getTotal())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupRespVO.java new file mode 100644 index 000000000..45e9c5f0a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 知识库分组 Response VO") +@Data +public class PmsKnowledgeGroupRespVO { + + @Schema(description = "知识库分组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "分组名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品知识库") + private String name; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sort; + + @Schema(description = "分组类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "分组内知识库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Integer libraryCount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupSaveReqVO.java new file mode 100644 index 000000000..5c567bf38 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupSaveReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库分组新增/修改 Request VO") +@Data +public class PmsKnowledgeGroupSaveReqVO { + + @Schema(description = "知识库分组编号", example = "1024") + private Long id; + + @Schema(description = "分组名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品知识库") + @NotBlank(message = "分组名称不能为空") + @Size(max = 100, message = "分组名称不能超过 100 个字符") + private String name; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupSortReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupSortReqVO.java new file mode 100644 index 000000000..d44f561f1 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeGroupSortReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库分组排序 Request VO") +@Data +public class PmsKnowledgeGroupSortReqVO { + + @Schema(description = "知识库分组排序项", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "知识库分组排序项不能为空") + private List<@Valid Item> items; + + @Schema(description = "管理后台 - PMS 知识库分组排序项 Request VO") + @Data + public static class Item { + + @Schema(description = "知识库分组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "知识库分组编号不能为空") + private Long id; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "显示顺序不能为空") + @Min(value = 0, message = "显示顺序不能小于 0") + private Integer sort; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeLibraryMoveGroupReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeLibraryMoveGroupReqVO.java new file mode 100644 index 000000000..4581020b8 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/group/PmsKnowledgeLibraryMoveGroupReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库移动分组 Request VO") +@Data +public class PmsKnowledgeLibraryMoveGroupReqVO { + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "知识库编号不能为空") + private Long libraryId; + + @Schema(description = "目标分组编号;为空表示移出分组", example = "2048") + private Long groupId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibraryPageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibraryPageReqVO.java new file mode 100644 index 000000000..1de242347 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibraryPageReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - PMS 知识库分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeLibraryPageReqVO extends PageParam { + + @Schema(description = "知识库名称", example = "产品") + private String name; + + @Schema(description = "个人知识库分组编号", example = "1024") + private Long groupId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibraryRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibraryRespVO.java new file mode 100644 index 000000000..20e6d434e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibraryRespVO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 知识库 Response VO") +@Data +public class PmsKnowledgeLibraryRespVO { + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品知识库") + private String name; + + @Schema(description = "知识库简介") + private String description; + + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean openStatus; + + @Schema(description = "知识库封面") + private String coverUrl; + + @Schema(description = "创建人用户编号", example = "1") + private Long creatorUserId; + + @Schema(description = "创建人姓名", example = "芋道源码") + private String creatorUserName; + + @Schema(description = "成员数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Integer memberCount; + + @Schema(description = "文档数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + private Long documentCount; + + @Schema(description = "文件数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") + private Long fileCount; + + @Schema(description = "当前用户是否可以编辑内容", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean writeStatus; + + @Schema(description = "当前用户是否为知识库管理员", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean adminStatus; + + @Schema(description = "当前用户是否可以主动退出知识库", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean exitStatus; + + @Schema(description = "当前用户是否已关注", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean favoriteStatus; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibrarySaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibrarySaveReqVO.java new file mode 100644 index 000000000..04daa730a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/library/PmsKnowledgeLibrarySaveReqVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库新增/修改 Request VO") +@Data +public class PmsKnowledgeLibrarySaveReqVO { + + @Schema(description = "知识库编号", example = "1024") + private Long id; + + @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品知识库") + @NotBlank(message = "知识库名称不能为空") + @Size(max = 50, message = "知识库名称不能超过 50 个字符") + private String name; + + @Schema(description = "知识库简介", example = "沉淀产品文档") + @Size(max = 300, message = "知识库简介不能超过 300 个字符") + private String description; + + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "知识库可见范围不能为空") + private Boolean openStatus; + + @Schema(description = "知识库封面") + @Size(max = 255, message = "知识库封面地址不能超过 255 个字符") + private String coverUrl; + + @Schema(description = "创建时的初始管理员用户编号列表", example = "[1, 2]") + private List adminUserIds; + + @Schema(description = "创建时的初始普通成员用户编号列表", example = "[3, 4]") + private List memberUserIds; + + @Schema(description = "模板编号,仅创建时有效", example = "1") + @Positive(message = "模板编号必须大于 0") + private Long templateId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/member/PmsKnowledgeLibraryMemberRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/member/PmsKnowledgeLibraryMemberRespVO.java new file mode 100644 index 000000000..3772f2789 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/member/PmsKnowledgeLibraryMemberRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.member; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库成员 Response VO") +@Data +public class PmsKnowledgeLibraryMemberRespVO { + + @Schema(description = "成员编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户编号", example = "1") + private Long userId; + + @Schema(description = "用户姓名", example = "芋道源码") + private String nickname; + + @Schema(description = "用户头像") + private String avatar; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "部门名称", example = "研发部") + private String deptName; + + @Schema(description = "父部门编号") + private Long parentDeptId; + + @Schema(description = "父部门名称") + private String parentDeptName; + + @Schema(description = "成员等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer level; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/member/PmsKnowledgeLibraryUpdateMemberListReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/member/PmsKnowledgeLibraryUpdateMemberListReqVO.java new file mode 100644 index 000000000..849db7a25 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/member/PmsKnowledgeLibraryUpdateMemberListReqVO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.member; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 更新知识库成员列表 Request VO") +@Data +public class PmsKnowledgeLibraryUpdateMemberListReqVO { + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "知识库编号不能为空") + private Long libraryId; + + @Schema(description = "成员列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "成员列表不能为空") + private List members; + + @Schema(description = "管理后台 - PMS 知识库成员更新项") + @Data + public static class Member { + + @Schema(description = "用户编号", example = "1") + private Long userId; + + @Schema(description = "部门编号", example = "100") + private Long deptId; + + @Schema(description = "成员等级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "成员等级不能为空") + @InEnum(PmsKnowledgeLibraryMemberLevelEnum.class) + private Integer level; + + @AssertTrue(message = "知识库成员必须且只能选择用户或部门") + @JsonIgnore + public boolean isUserOrDeptValid() { + return (userId != null && deptId == null) || (userId == null && deptId != null); + } + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplatePageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplatePageReqVO.java new file mode 100644 index 000000000..1a1aecf17 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplatePageReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - PMS 知识库模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PmsKnowledgeLibraryTemplatePageReqVO extends PageParam { + + @Schema(description = "模板名称,模糊匹配", example = "产品研发") + private String name; + + @Schema(description = "模板状态", example = "0") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplateRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplateRespVO.java new file mode 100644 index 000000000..f292aae76 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplateRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库模板 Response VO") +@Data +public class PmsKnowledgeLibraryTemplateRespVO { + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品研发") + private String name; + + @Schema(description = "模板简介", example = "提供完善的产品流程文档") + private String description; + + @Schema(description = "模板封面地址", example = "/static/pms/kb/template/product.png") + private String coverUrl; + + @Schema(description = "模板状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer sort; + + @Schema(description = "模板文档列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List documents; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplateSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplateSaveReqVO.java new file mode 100644 index 000000000..5dcf251de --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/library/vo/template/PmsKnowledgeLibraryTemplateSaveReqVO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库模板新增/修改 Request VO") +@Data +public class PmsKnowledgeLibraryTemplateSaveReqVO { + + @Schema(description = "模板编号", example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品研发") + @NotBlank(message = "模板名称不能为空") + @Size(max = 100, message = "模板名称不能超过 100 个字符") + private String name; + + @Schema(description = "模板简介", example = "提供完善的产品流程文档") + @Size(max = 500, message = "模板简介不能超过 500 个字符") + private String description; + + @Schema(description = "模板封面地址", example = "/static/pms/kb/template/product.png") + @Size(max = 255, message = "模板封面地址不能超过 255 个字符") + private String coverUrl; + + @Schema(description = "模板状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "模板状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "显示顺序不能为空") + @PositiveOrZero(message = "显示顺序不能小于 0") + private Integer sort; + + @Schema(description = "模板文档列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "模板文档不能为空") + @Valid + private List documents; + + @Schema(description = "模板文档") + @Data + public static class Document { + + @Schema(description = "文档标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品需求文档") + @NotBlank(message = "文档标题不能为空") + @Size(max = 255, message = "文档标题不能超过 255 个字符") + private String title; + + @Schema(description = "文档内容", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "文档内容不能为空") + private String content; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/PmsKnowledgeRecycleController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/PmsKnowledgeRecycleController.java new file mode 100644 index 000000000..7699700e7 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/PmsKnowledgeRecycleController.java @@ -0,0 +1,125 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.recycle; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo.PmsKnowledgeRecycleDetailRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo.PmsKnowledgeRecyclePreviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo.PmsKnowledgeRecycleRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.recycle.PmsKnowledgeRecycleRecordDO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import cn.iocoder.yudao.module.pms.service.kb.recycle.PmsKnowledgeRecycleService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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 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.PutMapping; +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 java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 知识库回收站") +@RestController +@RequestMapping("/pms/kb/recycle") +@Validated +public class PmsKnowledgeRecycleController { + + @Resource + private PmsKnowledgeRecycleService recycleService; + + @Resource + private AdminUserApi adminUserApi; + + @Resource + private PmsKnowledgeDocumentService documentService; + + @GetMapping("/library-list") + @Operation(summary = "获得已删除知识库列表") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult> getLibraryRecycleList() { + List records = recycleService.getLibraryRecycleList(getLoginUserId()); + return success(buildRecycleRespVOList(records)); + } + + @GetMapping("/content-list") + @Operation(summary = "获得知识库最近删除内容") + @Parameter(name = "libraryId", description = "知识库编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult> getContentRecycleList( + @RequestParam("libraryId") Long libraryId) { + List records = recycleService.getContentRecycleList( + libraryId, getLoginUserId()); + return success(buildRecycleRespVOList(records)); + } + + @GetMapping("/content-detail") + @Operation(summary = "获得知识库最近删除详情") + @Parameter(name = "id", description = "回收站记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult getContentRecycleDetail( + @RequestParam("id") Long id) { + return success(recycleService.getContentRecycleDetail(id, getLoginUserId())); + } + + @GetMapping("/content-preview") + @Operation(summary = "预览知识库最近删除内容") + @Parameter(name = "id", description = "回收站记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult getContentRecyclePreview( + @RequestParam("id") Long id, + @RequestParam(value = "entityId", required = false) Long entityId) { + PmsKnowledgeDocumentDO document = recycleService.getContentRecyclePreview(id, entityId, getLoginUserId()); + return success(BeanUtils.toBean(document, PmsKnowledgeRecyclePreviewRespVO.class, + o -> o.setName(document.getTitle()))); + } + + @PutMapping("/restore") + @Operation(summary = "恢复回收站对象") + @Parameter(name = "id", description = "回收站记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult restoreContentRecycle(@RequestParam("id") Long id) { + recycleService.restoreContentRecycle(id, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/permanent-delete") + @Operation(summary = "彻底删除回收站对象") + @Parameter(name = "id", description = "回收站记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:kb:library:delete')") + public CommonResult deleteContentRecycle(@RequestParam("id") Long id) { + recycleService.deleteContentRecycle(id, getLoginUserId()); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private List buildRecycleRespVOList(List records) { + Map userMap = adminUserApi.getUserMap( + convertSet(records, PmsKnowledgeRecycleRecordDO::getDeleteUserId)); + Map documentMap = documentService.getDocumentMap(convertSet(records, record -> + PmsKnowledgeObjectTypeEnum.FILE.getType().equals(record.getType()) ? record.getEntityId() : null)); + return convertList(records, record -> { + PmsKnowledgeRecycleRespVO recordVO = BeanUtils.toBean(record, PmsKnowledgeRecycleRespVO.class); + findAndThen(documentMap, record.getEntityId(), document -> recordVO.setFileSize(document.getFileSize())); + findAndThen(userMap, record.getDeleteUserId(), user -> recordVO.setDeleteUserName(user.getNickname())); + return recordVO; + }); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecycleDetailRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecycleDetailRespVO.java new file mode 100644 index 000000000..86f1f2553 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecycleDetailRespVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 知识库回收站详情 Response VO") +@Data +public class PmsKnowledgeRecycleDetailRespVO { + + @Schema(description = "根回收站记录") + private PmsKnowledgeRecycleRespVO root; + + @Schema(description = "级联删除内容") + private List children; + + @Data + public static class Item { + + @Schema(description = "内容编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "内容类型", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer type; + + @Schema(description = "内容名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "父文档或父文件夹编号") + private Long parentId; + + @Schema(description = "所属文件夹编号") + private Long folderId; + + @Schema(description = "删除时间") + private LocalDateTime deleteTime; + + } +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecyclePreviewRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecyclePreviewRespVO.java new file mode 100644 index 000000000..9f10dfc6d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecyclePreviewRespVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 知识库回收站内容预览 Response VO") +@Data +public class PmsKnowledgeRecyclePreviewRespVO { + + @Schema(description = "内容编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "内容类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "内容名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品说明") + private String name; + + @Schema(description = "文档正文或文件地址") + private String content; + + @Schema(description = "文件类型", example = "pdf") + private String fileType; + + @Schema(description = "文件大小,单位:字节", example = "102400") + private Long fileSize; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecycleRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecycleRespVO.java new file mode 100644 index 000000000..c561770b0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/kb/recycle/vo/PmsKnowledgeRecycleRespVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 知识库回收站记录 Response VO") +@Data +public class PmsKnowledgeRecycleRespVO { + + @Schema(description = "回收站记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long libraryId; + + @Schema(description = "对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "对象编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long entityId; + + @Schema(description = "对象名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品说明") + private String name; + + @Schema(description = "文件大小,单位:字节") + private Long fileSize; + + @Schema(description = "删除人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long deleteUserId; + + @Schema(description = "删除人姓名", example = "芋道") + private String deleteUserName; + + @Schema(description = "删除时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime deleteTime; + + @Schema(description = "父对象编号,回收站详情中用于组装层级", example = "2048") + private Long parentId; + + @Schema(description = "所属文件夹编号,回收站详情中用于组装层级", example = "2048") + private Long folderId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/PmsIterationController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/PmsIterationController.java new file mode 100644 index 000000000..06f215bcd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/PmsIterationController.java @@ -0,0 +1,152 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.iteration; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationOverviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationStartReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 项目迭代") +@RestController +@RequestMapping("/pms/pm/iteration") +@Validated +public class PmsIterationController { + + @Resource + private PmsIterationService iterationService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建项目迭代") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:create')") + public CommonResult createIteration(@Valid @RequestBody PmsIterationSaveReqVO saveReqVO) { + return success(iterationService.createIteration(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新项目迭代") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:update')") + public CommonResult updateIteration(@Valid @RequestBody PmsIterationSaveReqVO saveReqVO) { + iterationService.updateIteration(saveReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/start") + @Operation(summary = "开始项目迭代") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:update')") + public CommonResult startIteration(@Valid @RequestBody PmsIterationStartReqVO startReqVO) { + iterationService.startIteration(startReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/complete") + @Operation(summary = "完成项目迭代") + @Parameter(name = "id", description = "迭代编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:update')") + public CommonResult completeIteration(@RequestParam("id") Long id) { + iterationService.completeIteration(id, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除项目迭代") + @Parameter(name = "id", description = "迭代编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:delete')") + public CommonResult deleteIteration(@RequestParam("id") Long id) { + iterationService.deleteIteration(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得项目迭代详情") + @Parameter(name = "id", description = "迭代编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:query')") + public CommonResult getIteration(@RequestParam("id") Long id) { + PmsIterationDO iteration = iterationService.getIteration(id, getLoginUserId()); + PmsIterationRespVO iterationVO = buildIterationRespVO(iteration); + return success(iterationVO); + } + + @GetMapping("/page") + @Operation(summary = "获得项目迭代分页") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:query')") + public CommonResult> getIterationPage( + @Valid PmsIterationPageReqVO pageReqVO) { + PageResult pageResult = iterationService.getIterationPage(pageReqVO, getLoginUserId()); + List iterationVOList = buildIterationRespVOList(pageResult.getList()); + Map progressMap = iterationService.getIterationProgressMap( + convertSet(pageResult.getList(), PmsIterationDO::getId)); + iterationVOList.forEach(iteration -> iteration.setProgress(progressMap.getOrDefault(iteration.getId(), 0))); + return success(new PageResult<>(iterationVOList, pageResult.getTotal())); + } + + @GetMapping("/overview") + @Operation(summary = "获得迭代概览统计") + @Parameter(name = "id", description = "迭代编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:query')") + public CommonResult getIterationOverview(@RequestParam("id") Long id) { + PmsIterationOverviewRespVO overview = iterationService.getIterationOverview(id, getLoginUserId()); + List activities = overview.getRecentActivities(); + Map activityUserMap = adminUserApi.getUserMap( + convertSet(activities, PmsIterationOverviewRespVO.ActivityItem::getOperatorUserId)); + activities.forEach(activity -> findAndThen(activityUserMap, activity.getOperatorUserId(), + user -> activity.setOperatorUserName(user.getNickname()))); + return success(overview.setRecentActivities(activities)); + } + + // ==================== 拼接 VO ==================== + + private PmsIterationRespVO buildIterationRespVO(PmsIterationDO iteration) { + return CollUtil.getFirst(buildIterationRespVOList(Collections.singletonList(iteration))); + } + + private List buildIterationRespVOList(List iterations) { + if (CollUtil.isEmpty(iterations)) { + return Collections.emptyList(); + } + + // 1. 批量查询负责人 + Map userMap = adminUserApi.getUserMap( + convertSet(iterations, PmsIterationDO::getOwnerUserId)); + + // 2. 转换迭代响应,并拼接负责人姓名 + return BeanUtils.toBean(iterations, PmsIterationRespVO.class, iterationVO -> + findAndThen(userMap, iterationVO.getOwnerUserId(), + user -> iterationVO.setOwnerUserName(user.getNickname()))); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationOverviewRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationOverviewRespVO.java new file mode 100644 index 000000000..98eacc198 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationOverviewRespVO.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - PMS 迭代概览 Response VO") +@Data +public class PmsIterationOverviewRespVO { + + @Schema(description = "工作项总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer totalCount; + + @Schema(description = "未开始工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer pendingCount; + + @Schema(description = "进行中工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") + private Integer processingCount; + + @Schema(description = "已完成工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer completedCount; + + @Schema(description = "完成进度百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer progress; + + @Schema(description = "工作项类型数量统计", requiredMode = Schema.RequiredMode.REQUIRED, example = "{\"1\": 60, \"2\": 40}") + private Map typeCountMap; + + @Schema(description = "工作项类型和状态数量统计", requiredMode = Schema.RequiredMode.REQUIRED) + private Map> typeStatusCountMap; + + @Schema(description = "状态趋势", requiredMode = Schema.RequiredMode.REQUIRED) + private List statusTrends; + + @Schema(description = "燃尽图数据", requiredMode = Schema.RequiredMode.REQUIRED) + private List burnDowns; + + @Schema(description = "最近动态列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List recentActivities; + + @Schema(description = "管理后台 - PMS 迭代概览状态趋势项 VO") + @Data + public static class TrendItem { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-01-01") + private String date; + + @Schema(description = "当日未开始工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Integer pendingCount; + + @Schema(description = "当日进行中工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer processingCount; + + @Schema(description = "当日已完成工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer completedCount; + + } + + @Schema(description = "管理后台 - PMS 迭代概览燃尽图项 VO") + @Data + public static class BurnDownItem { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-01-01") + private String date; + + @Schema(description = "理想剩余工时,单位:小时", requiredMode = Schema.RequiredMode.REQUIRED, example = "40") + private Integer idealRemaining; + + @Schema(description = "实际剩余工时,单位:小时", requiredMode = Schema.RequiredMode.REQUIRED, example = "38") + private Integer actualRemaining; + + } + + @Schema(description = "管理后台 - PMS 迭代概览动态项 VO") + @Data + public static class ActivityItem { + + @Schema(description = "动态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long workItemId; + + @Schema(description = "工作项序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer workItemSerialNumber; + + @Schema(description = "工作项名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "优化登录页") + private String workItemName; + + @Schema(description = "操作人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long operatorUserId; + + @Schema(description = "操作人昵称", example = "芋道") + private String operatorUserName; + + @Schema(description = "动态内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "将状态更新为「已完成」") + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationPageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationPageReqVO.java new file mode 100644 index 000000000..588fa4393 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationPageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.iteration.PmsIterationStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - PMS 项目迭代分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsIterationPageReqVO extends PageParam { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "迭代名称", example = "第一期") + private String name; + + @Schema(description = "迭代状态", example = "1") + @InEnum(PmsIterationStatusEnum.class) + private Integer status; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationRespVO.java new file mode 100644 index 000000000..2d0e40143 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationRespVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 项目迭代 Response VO") +@Data +public class PmsIterationRespVO { + + @Schema(description = "迭代编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long projectId; + + @Schema(description = "迭代名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "第一期") + private String name; + + @Schema(description = "负责人用户编号", example = "1") + private Long ownerUserId; + + @Schema(description = "迭代状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "负责人姓名", example = "芋道源码") + private String ownerUserName; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "结束时间") + private LocalDateTime endTime; + + @Schema(description = "迭代目标", example = "完成核心流程") + private String target; + + @Schema(description = "迭代描述") + private String description; + + @Schema(description = "完成时间") + private LocalDateTime finishTime; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sort; + + @Schema(description = "完成进度百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer progress; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationSaveReqVO.java new file mode 100644 index 000000000..6e3bf5b17 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationSaveReqVO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 项目迭代新增/修改 Request VO") +@Data +public class PmsIterationSaveReqVO { + + @Schema(description = "迭代编号", example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "迭代名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "第一期") + @NotBlank(message = "迭代名称不能为空") + @Size(max = 100, message = "迭代名称长度不能超过 100 个字符") + private String name; + + @Schema(description = "负责人用户编号", example = "1") + private Long ownerUserId; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "结束时间") + private LocalDateTime endTime; + + @Schema(description = "迭代目标", example = "完成核心流程") + @Size(max = 255, message = "迭代目标长度不能超过 255 个字符") + private String target; + + @Schema(description = "迭代描述") + private String description; + + @Schema(hidden = true) + @AssertTrue(message = "迭代结束时间必须晚于开始时间,且开始、结束时间需要同时填写") + @JsonIgnore + public boolean isTimeRangeValid() { + return startTime == null && endTime == null + || startTime != null && endTime != null && endTime.isAfter(startTime); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationStartReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationStartReqVO.java new file mode 100644 index 000000000..dcfe68c12 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/iteration/vo/PmsIterationStartReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 项目迭代开始 Request VO") +@Data +public class PmsIterationStartReqVO { + + @Schema(description = "迭代编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "迭代编号不能为空") + private Long id; + + @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "迭代开始时间不能为空") + private LocalDateTime startTime; + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "迭代结束时间不能为空") + private LocalDateTime endTime; + + @Schema(hidden = true) + @AssertTrue(message = "迭代结束时间必须晚于开始时间") + @JsonIgnore + public boolean isTimeRangeValid() { + return startTime == null || endTime == null || endTime.isAfter(startTime); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectAnnouncementController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectAnnouncementController.java new file mode 100644 index 000000000..896afaaa0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectAnnouncementController.java @@ -0,0 +1,129 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.announcement.PmsProjectAnnouncementRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.announcement.PmsProjectAnnouncementSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectAnnouncementDO; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectAnnouncementService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 项目公告") +@RestController +@RequestMapping("/pms/pm/project-announcement") +@Validated +public class PmsProjectAnnouncementController { + + @Resource + private PmsProjectAnnouncementService projectAnnouncementService; + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建项目公告") + @PreAuthorize("@ss.hasPermission('pms:pm:project:update')") + public CommonResult createProjectAnnouncement(@Valid @RequestBody PmsProjectAnnouncementSaveReqVO saveReqVO) { + return success(projectAnnouncementService.createProjectAnnouncement(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新项目公告") + @PreAuthorize("@ss.hasPermission('pms:pm:project:update')") + public CommonResult updateProjectAnnouncement( + @Valid @RequestBody PmsProjectAnnouncementSaveReqVO saveReqVO) { + projectAnnouncementService.updateProjectAnnouncement(saveReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除项目公告") + @Parameter(name = "id", description = "公告编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:update')") + public CommonResult deleteProjectAnnouncement(@RequestParam("id") Long id) { + projectAnnouncementService.deleteProjectAnnouncement(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得项目公告详情") + @Parameter(name = "id", description = "公告编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:query')") + public CommonResult getProjectAnnouncement(@RequestParam("id") Long id) { + PmsProjectAnnouncementDO announcement = projectAnnouncementService + .getProjectAnnouncement(id, getLoginUserId()); + return success(buildProjectAnnouncementRespVO(announcement)); + } + + @GetMapping("/list") + @Operation(summary = "获得项目公告列表") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:query')") + public CommonResult> getProjectAnnouncementList( + @RequestParam("projectId") Long projectId) { + List announcements = projectAnnouncementService + .getProjectAnnouncementList(projectId, getLoginUserId()); + List announcementVOList = buildProjectAnnouncementRespVOList(announcements); + return success(announcementVOList); + } + + // ==================== 拼接 VO ==================== + + /** + * 构建项目公告响应 + * + * @param announcement 项目公告 + * @return 项目公告响应 + */ + private PmsProjectAnnouncementRespVO buildProjectAnnouncementRespVO(PmsProjectAnnouncementDO announcement) { + return CollUtil.getFirst(buildProjectAnnouncementRespVOList(Collections.singletonList(announcement))); + } + + /** + * 构建项目公告响应列表 + * + * @param announcements 项目公告列表 + * @return 项目公告响应列表 + */ + private List buildProjectAnnouncementRespVOList( + List announcements) { + Map userMap = adminUserApi.getUserMap(convertSet(announcements, + announcement -> NumberUtils.parseLong(announcement.getCreator()))); + return convertList(announcements, announcement -> { + PmsProjectAnnouncementRespVO announcementVO = BeanUtils.toBean( + announcement, PmsProjectAnnouncementRespVO.class) + .setCreatorUserId(NumberUtils.parseLong(announcement.getCreator())); + findAndThen(userMap, announcementVO.getCreatorUserId(), + user -> announcementVO.setCreatorUserName(user.getNickname())); + return announcementVO; + }); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectController.java new file mode 100644 index 000000000..d6ea74a09 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectController.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectOverviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectFavoriteService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 项目") +@RestController +@RequestMapping("/pms/pm/project") +@Validated +public class PmsProjectController { + + @Resource + private PmsProjectService projectService; + @Resource + private PmsProjectMemberService projectMemberService; + @Resource + private PmsProjectFavoriteService projectFavoriteService; + @Resource + private PmsWorkItemService workItemService; + @Resource + private AdminUserApi adminUserApi; + @Resource + private PermissionApi permissionApi; + + @PostMapping("/create") + @Operation(summary = "创建项目") + @PreAuthorize("@ss.hasPermission('pms:pm:project:create')") + public CommonResult createProject(@Valid @RequestBody PmsProjectSaveReqVO saveReqVO) { + return success(projectService.createProject(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新项目") + @PreAuthorize("@ss.hasPermission('pms:pm:project:update')") + public CommonResult updateProject(@Valid @RequestBody PmsProjectSaveReqVO saveReqVO) { + projectService.updateProject(saveReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得项目详情") + @Parameter(name = "id", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:query')") + public CommonResult getProject(@RequestParam("id") Long id) { + Long userId = getLoginUserId(); + PmsProjectDO project = projectService.getProjectAndUpdateVisitTime(id, userId); + return success(buildProjectRespVO(project, userId)); + } + + @GetMapping("/page") + @Operation(summary = "获得项目分页") + @PreAuthorize("@ss.hasPermission('pms:pm:project:query')") + public CommonResult> getProjectPage(@Valid PmsProjectPageReqVO pageReqVO) { + Long userId = getLoginUserId(); + PageResult pageResult = projectService.getProjectPage(pageReqVO, userId); + return success(new PageResult<>(buildProjectRespVOList(pageResult.getList(), userId), pageResult.getTotal())); + } + + @GetMapping("/favorite-list") + @Operation(summary = "获得星标项目列表") + @PreAuthorize("@ss.hasPermission('pms:pm:project:query')") + public CommonResult> getFavoriteProjectList() { + Long userId = getLoginUserId(); + return success(buildProjectRespVOList(projectService.getFavoriteProjectList(userId), userId)); + } + + @GetMapping("/overview") + @Operation(summary = "获得项目概况") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:query')") + public CommonResult getProjectOverview(@RequestParam("projectId") Long projectId) { + return success(projectService.getProjectOverview(projectId, getLoginUserId())); + } + + @PutMapping("/archive") + @Operation(summary = "归档项目") + @Parameter(name = "id", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:update')") + public CommonResult archiveProject(@RequestParam("id") Long id) { + projectService.archiveProject(id, getLoginUserId()); + return success(true); + } + + @PutMapping("/recycle") + @Operation(summary = "将项目移入回收站") + @Parameter(name = "id", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:update')") + public CommonResult recycleProject(@RequestParam("id") Long id) { + projectService.recycleProject(id, getLoginUserId()); + return success(true); + } + + @PutMapping("/restore") + @Operation(summary = "恢复项目") + @Parameter(name = "id", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:update')") + public CommonResult restoreProject(@RequestParam("id") Long id) { + projectService.restoreProject(id, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "彻底删除回收站项目") + @Parameter(name = "id", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:delete')") + public CommonResult deleteProject(@RequestParam("id") Long id) { + projectService.deleteProject(id, getLoginUserId()); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private PmsProjectRespVO buildProjectRespVO(PmsProjectDO project, Long userId) { + if (project == null) { + return null; + } + return CollUtil.getFirst(buildProjectRespVOList(Collections.singletonList(project), userId)); + } + + private List buildProjectRespVOList(List projects, Long userId) { + if (CollUtil.isEmpty(projects)) { + return Collections.emptyList(); + } + // 1. 批量查询创建人、项目管理员、成员数量和工作项完成度 + Set projectIds = convertSet(projects, PmsProjectDO::getId); + Map> managerUserIdListMap = projectMemberService.getProjectManagerUserIdListMap(projectIds); + Map memberCountMap = projectMemberService.getProjectMemberCountMap(projectIds); + Map> projectStatusCountMap = workItemService + .getProjectWorkItemStatusCountMap(projectIds); + Set userIds = convertSet(projects, project -> + NumberUtils.parseLong(project.getCreator())); + managerUserIdListMap.values().forEach(userIds::addAll); + Map userMap = adminUserApi.getUserMap(userIds); + Set ownerProjectIds = convertSet(projectMemberService.getOwnerProjectIdListByUserId(userId)); + Set managedProjectIds = convertSet(projectMemberService.getManagedProjectIdListByUserId(userId)); + Set memberProjectIds = convertSet(projectMemberService.getProjectIdListByUserId(userId)); + Set writableProjectIds = convertSet(projectMemberService.getWritableProjectIdListByUserId(userId)); + Set favoriteProjectIds = convertSet(projectFavoriteService.getFavoriteProjectIdListByUserId(userId)); + favoriteProjectIds.retainAll(projectIds); + boolean superAdmin = permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData(); + // 2. 转换项目响应,并拼接展示字段 + return BeanUtils.toBean(projects, PmsProjectRespVO.class, projectVO -> { + findAndThen(userMap, NumberUtils.parseLong(projectVO.getCreator()), + creator -> projectVO.setCreatorName(creator.getNickname())); + List adminNames = new ArrayList<>(); + for (Long adminUserId : managerUserIdListMap.getOrDefault(projectVO.getId(), Collections.emptyList())) { + findAndThen(userMap, adminUserId, admin -> adminNames.add(admin.getNickname())); + } + Map statusCountMap = projectStatusCountMap.getOrDefault( + projectVO.getId(), Collections.emptyMap()); + projectVO.setAdminNames(adminNames).setMemberCount(memberCountMap.getOrDefault(projectVO.getId(), 0)) + .setPendingWorkItemCount(statusCountMap.getOrDefault(PmsWorkItemStatusTypeEnum.PENDING.getType(), 0L)) + .setProcessingWorkItemCount(statusCountMap.getOrDefault(PmsWorkItemStatusTypeEnum.PROCESSING.getType(), 0L)) + .setCompletedWorkItemCount(statusCountMap.getOrDefault(PmsWorkItemStatusTypeEnum.COMPLETED.getType(), 0L)) + .setOwnerStatus(superAdmin || ownerProjectIds.contains(projectVO.getId())) + .setAdminStatus(superAdmin || managedProjectIds.contains(projectVO.getId())) + .setMemberStatus(memberProjectIds.contains(projectVO.getId())) + .setExitStatus(memberProjectIds.contains(projectVO.getId()) && !ownerProjectIds.contains(projectVO.getId())) + .setWriteStatus(superAdmin || writableProjectIds.contains(projectVO.getId())) + .setFavoriteStatus(favoriteProjectIds.contains(projectVO.getId())); + }); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectFavoriteController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectFavoriteController.java new file mode 100644 index 000000000..85db30053 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectFavoriteController.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectFavoriteService; +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 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.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 项目收藏") +@RestController +@RequestMapping("/pms/pm/project-favorite") +@Validated +public class PmsProjectFavoriteController { + + @Resource + private PmsProjectFavoriteService projectFavoriteService; + + @PostMapping("/create") + @Operation(summary = "收藏项目") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:query')") + public CommonResult createProjectFavorite(@RequestParam("projectId") Long projectId) { + projectFavoriteService.createProjectFavorite(projectId, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "取消收藏项目") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project:query')") + public CommonResult deleteProjectFavorite(@RequestParam("projectId") Long projectId) { + projectFavoriteService.deleteProjectFavorite(projectId, getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectGroupController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectGroupController.java new file mode 100644 index 000000000..ca2214da1 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectGroupController.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectGroupDO; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectGroupService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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 java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 项目分组") +@RestController +@RequestMapping("/pms/pm/project-group") +@Validated +public class PmsProjectGroupController { + + @Resource + private PmsProjectGroupService projectGroupService; + @Resource + private PmsProjectMemberService projectMemberService; + + @PostMapping("/create") + @Operation(summary = "创建项目分组") + @PreAuthorize("@ss.hasPermission('pms:pm:project-group:create')") + public CommonResult createProjectGroup(@Valid @RequestBody PmsProjectGroupSaveReqVO createReqVO) { + return success(projectGroupService.createProjectGroup(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改项目分组") + @PreAuthorize("@ss.hasPermission('pms:pm:project-group:update')") + public CommonResult updateProjectGroup(@Valid @RequestBody PmsProjectGroupSaveReqVO updateReqVO) { + projectGroupService.updateProjectGroup(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-sort") + @Operation(summary = "修改项目分组排序") + @PreAuthorize("@ss.hasPermission('pms:pm:project-group:update')") + public CommonResult updateProjectGroupSort(@Valid @RequestBody PmsProjectGroupSortReqVO sortReqVO) { + projectGroupService.updateProjectGroupSort(sortReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除项目分组") + @Parameter(name = "id", description = "项目分组编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project-group:delete')") + public CommonResult deleteProjectGroup(@RequestParam("id") Long id) { + projectGroupService.deleteProjectGroup(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得项目分组列表") + @PreAuthorize("@ss.hasPermission('pms:pm:project-group:query')") + public CommonResult> getProjectGroupList() { + Long userId = getLoginUserId(); + List groups = projectGroupService.getProjectGroupList(userId); + List projectIds = projectMemberService.getActiveProjectIdListByUserId(userId); + Map groupCountMap = projectGroupService.getProjectGroupCountMap(userId, groups, projectIds); + return success(BeanUtils.toBean(groups, PmsProjectGroupRespVO.class, + group -> group.setProjectCount(groupCountMap.getOrDefault(group.getId(), 0)))); + } + + @PutMapping("/move-project") + @Operation(summary = "移动项目到个人分组") + @PreAuthorize("@ss.hasPermission('pms:pm:project-group:update')") + public CommonResult moveProjectToGroup(@Valid @RequestBody PmsProjectGroupMoveReqVO moveReqVO) { + projectGroupService.moveProjectToGroup(moveReqVO, getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectMemberController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectMemberController.java new file mode 100644 index 000000000..40f8840f6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectMemberController.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member.PmsProjectMemberRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member.PmsProjectMemberSaveReqVO; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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.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.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 项目成员") +@RestController +@RequestMapping("/pms/pm/project-member") +@Validated +public class PmsProjectMemberController { + + @Resource + private PmsProjectMemberService projectMemberService; + + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/list") + @Operation(summary = "获得项目成员列表") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project-member:query')") + public CommonResult> getProjectMemberList( + @RequestParam("projectId") Long projectId) { + List members = projectMemberService.getProjectMemberList(projectId, getLoginUserId()); + return success(fillProjectMemberRespVOList(members)); + } + + @PutMapping("/update-list") + @Operation(summary = "保存项目成员及其权限级别") + @PreAuthorize("@ss.hasPermission('pms:pm:project-member:update')") + public CommonResult updateProjectMemberList(@Valid @RequestBody PmsProjectMemberSaveReqVO saveReqVO) { + projectMemberService.updateProjectMemberList(saveReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "移除项目成员") + @Parameters({ + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024"), + @Parameter(name = "userId", description = "后台用户编号", required = true, example = "1") + }) + @PreAuthorize("@ss.hasPermission('pms:pm:project-member:update')") + public CommonResult deleteProjectMember(@RequestParam("projectId") Long projectId, + @RequestParam("userId") Long userId) { + projectMemberService.deleteProjectMember(projectId, userId, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/exit") + @Operation(summary = "主动退出项目") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project-member:query')") + public CommonResult exitProject(@RequestParam("projectId") Long projectId) { + projectMemberService.exitProject(projectId, getLoginUserId()); + return success(true); + } + + // ==================== 拼接 VO ==================== + + private List fillProjectMemberRespVOList(List members) { + if (CollUtil.isEmpty(members)) { + return Collections.emptyList(); + } + // 1. 批量查询用户 + Map userMap = adminUserApi.getUserMap( + convertSet(members, PmsProjectMemberRespVO::getUserId)); + // 2. 拼接用户信息 + for (PmsProjectMemberRespVO memberVO : members) { + MapUtils.findAndThen(userMap, memberVO.getUserId(), + user -> memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar())); + } + return members; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectTemplateController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectTemplateController.java new file mode 100644 index 000000000..cb7e932b0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/PmsProjectTemplateController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplatePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplateRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplateSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectTemplateDO; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectTemplateService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - PMS 项目模板") +@RestController +@RequestMapping("/pms/pm/project-template") +@Validated +public class PmsProjectTemplateController { + + @Resource + private PmsProjectTemplateService projectTemplateService; + + @PostMapping("/create") + @Operation(summary = "创建项目模板") + @PreAuthorize("@ss.hasPermission('pms:pm:project-template:create')") + public CommonResult createProjectTemplate(@Valid @RequestBody PmsProjectTemplateSaveReqVO createReqVO) { + return success(projectTemplateService.createProjectTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新项目模板") + @PreAuthorize("@ss.hasPermission('pms:pm:project-template:update')") + public CommonResult updateProjectTemplate(@Valid @RequestBody PmsProjectTemplateSaveReqVO updateReqVO) { + projectTemplateService.updateProjectTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除项目模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project-template:delete')") + public CommonResult deleteProjectTemplate(@RequestParam("id") Long id) { + projectTemplateService.deleteProjectTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得项目模板") + @Parameter(name = "id", description = "模板编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:project-template:query')") + public CommonResult getProjectTemplate(@RequestParam("id") Long id) { + PmsProjectTemplateDO template = projectTemplateService.getProjectTemplate(id); + return success(BeanUtils.toBean(template, PmsProjectTemplateRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得项目模板分页") + @PreAuthorize("@ss.hasPermission('pms:pm:project-template:query')") + public CommonResult> getProjectTemplatePage( + @Valid PmsProjectTemplatePageReqVO pageReqVO) { + PageResult pageResult = projectTemplateService.getProjectTemplatePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, PmsProjectTemplateRespVO.class)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/announcement/PmsProjectAnnouncementRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/announcement/PmsProjectAnnouncementRespVO.java new file mode 100644 index 000000000..3c9e4f88f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/announcement/PmsProjectAnnouncementRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.announcement; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 项目公告 Response VO") +@Data +public class PmsProjectAnnouncementRespVO { + + @Schema(description = "公告编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long projectId; + + @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "项目启动") + private String content; + + @Schema(description = "附件地址列表", example = "[\"https://example.com/a.pdf\"]") + private List fileUrls; + + @Schema(description = "创建者用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long creatorUserId; + + @Schema(description = "创建者用户昵称", example = "芋道") + private String creatorUserName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/announcement/PmsProjectAnnouncementSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/announcement/PmsProjectAnnouncementSaveReqVO.java new file mode 100644 index 000000000..f3b280a6b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/announcement/PmsProjectAnnouncementSaveReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.announcement; + +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.util.List; + +@Schema(description = "管理后台 - PMS 项目公告保存 Request VO") +@Data +public class PmsProjectAnnouncementSaveReqVO { + + @Schema(description = "公告编号", example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "公告内容不能为空") + @Size(max = 5000, message = "公告内容不能超过 5000 个字符") + private String content; + + @Schema(description = "附件地址列表") + private List fileUrls; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupMoveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupMoveReqVO.java new file mode 100644 index 000000000..58a74b009 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupMoveReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 项目移动分组 Request VO") +@Data +public class PmsProjectGroupMoveReqVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "目标项目分组编号;不传表示移动到未分组", example = "2048") + private Long groupId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupRespVO.java new file mode 100644 index 000000000..c46dbef0f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 项目分组 Response VO") +@Data +public class PmsProjectGroupRespVO { + + @Schema(description = "项目分组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "分组名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "重点项目") + private String name; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sort; + + @Schema(description = "分组类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "项目数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Integer projectCount; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupSaveReqVO.java new file mode 100644 index 000000000..1f368f4c5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupSaveReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 项目分组新增/修改 Request VO") +@Data +public class PmsProjectGroupSaveReqVO { + + @Schema(description = "项目分组编号", example = "1024") + private Long id; + + @Schema(description = "分组名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "重点项目") + @NotBlank(message = "分组名称不能为空") + @Size(max = 100, message = "分组名称不能超过 100 个字符") + private String name; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupSortReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupSortReqVO.java new file mode 100644 index 000000000..464b5f8e1 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/group/PmsProjectGroupSortReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 项目分组排序 Request VO") +@Data +public class PmsProjectGroupSortReqVO { + + @Schema(description = "项目分组排序项", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "项目分组排序项不能为空") + private List items; + + @Schema(description = "管理后台 - PMS 项目分组排序项") + @Data + public static class Item { + + @Schema(description = "项目分组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目分组编号不能为空") + private Long id; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "显示顺序不能为空") + @Min(value = 0, message = "显示顺序不能小于 0") + private Integer sort; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/member/PmsProjectMemberRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/member/PmsProjectMemberRespVO.java new file mode 100644 index 000000000..7913faa70 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/member/PmsProjectMemberRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 项目成员 Response VO") +@Data +public class PmsProjectMemberRespVO { + + @Schema(description = "后台用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long userId; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String nickname; + + @Schema(description = "用户头像") + private String avatar; + + @Schema(description = "成员权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer level; + + @Schema(description = "是否项目创建人", example = "true") + private Boolean creatorStatus; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/member/PmsProjectMemberSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/member/PmsProjectMemberSaveReqVO.java new file mode 100644 index 000000000..cde7b2468 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/member/PmsProjectMemberSaveReqVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectMemberLevelEnum; +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; + +@Schema(description = "管理后台 - PMS 项目成员保存 Request VO") +@Data +public class PmsProjectMemberSaveReqVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "项目成员列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "项目成员不能为空") + @Valid + private List members; + + @Schema(description = "管理后台 - PMS 项目成员保存项") + @Data + public static class Member { + + @Schema(description = "后台用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "后台用户编号不能为空") + private Long userId; + + @Schema(description = "成员权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "成员权限级别不能为空") + @InEnum(PmsProjectMemberLevelEnum.class) + private Integer level; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectOverviewRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectOverviewRespVO.java new file mode 100644 index 000000000..b1a9d5e1f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectOverviewRespVO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - PMS 项目概况 Response VO") +@Data +public class PmsProjectOverviewRespVO { + + @Schema(description = "工作项总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Long totalCount; + + @Schema(description = "未开始工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "8") + private Long pendingCount; + + @Schema(description = "进行中工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "6") + private Long processingCount; + + @Schema(description = "已完成工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "6") + private Long completedCount; + + @Schema(description = "按工作项类型统计") + private Map typeCountMap; + + @Schema(description = "近十四日完成趋势") + private List completedTrends; + + @Schema(description = "分配给当前用户的未完成工作项") + private List assignedWorkItems; + + @Schema(description = "PMS 项目概况 - 趋势点") + @Data + public static class TrendPoint { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private String date; + + @Schema(description = "当日完成数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Long count; + + } + + @Schema(description = "PMS 项目概况 - 分配给我的工作项") + @Data + public static class AssignedWorkItem { + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目内序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + private Integer serialNumber; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "工作项标题", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "语义状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer status; + + @Schema(description = "截止时间") + private LocalDateTime endTime; + + @Schema(description = "完成进度", requiredMode = Schema.RequiredMode.REQUIRED, example = "60") + private Integer progress; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectPageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectPageReqVO.java new file mode 100644 index 000000000..86832d482 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectPageReqVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectSceneTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectSortTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - PMS 项目分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsProjectPageReqVO extends PageParam { + + @Schema(description = "项目名称", example = "官网") + private String name; + + @Schema(description = "列表场景", example = "1") + @InEnum(PmsProjectSceneTypeEnum.class) + private Integer sceneType = PmsProjectSceneTypeEnum.ALL.getType(); + + @Schema(description = "个人项目分组编号", example = "1024") + private Long groupId; + + @Schema(description = "项目状态", example = "1") + @InEnum(PmsProjectStatusEnum.class) + private Integer status = PmsProjectStatusEnum.ACTIVE.getStatus(); + + @Schema(description = "排序类型", example = "1") + @InEnum(PmsProjectSortTypeEnum.class) + private Integer sortType = PmsProjectSortTypeEnum.ACCESS_TIME.getType(); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectRespVO.java new file mode 100644 index 000000000..0dbefef00 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectRespVO.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 项目 Response VO") +@Data +public class PmsProjectRespVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "官网重构") + private String name; + + @Schema(description = "项目状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "项目优先级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer level; + + @Schema(description = "项目描述", example = "完成官网前后端重构") + private String description; + + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean openStatus; + + @Schema(description = "项目图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "ep:folder") + private String icon; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "截止时间") + private LocalDateTime endTime; + + @Schema(description = "归档时间") + private LocalDateTime archiveTime; + + @Schema(description = "移入回收站时间") + private LocalDateTime recycleTime; + + @Schema(description = "最近访问时间") + private LocalDateTime accessTime; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "创建人编号", example = "1") + private String creator; + + @Schema(description = "创建人姓名", example = "芋道源码") + private String creatorName; + + @Schema(description = "项目管理员姓名列表") + private List adminNames; + + @Schema(description = "项目成员数量", example = "5") + private Integer memberCount; + + @Schema(description = "未开始工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Long pendingWorkItemCount; + + @Schema(description = "进行中工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Long processingWorkItemCount; + + @Schema(description = "已完成工作项数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Long completedWorkItemCount; + + @Schema(description = "当前用户是否为项目管理员", example = "true") + private Boolean adminStatus; + + @Schema(description = "当前用户是否拥有项目所有者权限", example = "true") + private Boolean ownerStatus; + + @Schema(description = "当前用户是否为项目成员", example = "true") + private Boolean memberStatus; + + @Schema(description = "当前用户是否可以主动退出项目", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean exitStatus; + + @Schema(description = "当前用户是否可以编辑项目业务数据", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean writeStatus; + + @Schema(description = "当前用户是否已收藏", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean favoriteStatus; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectSaveReqVO.java new file mode 100644 index 000000000..8094bc2b9 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/project/PmsProjectSaveReqVO.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectLevelEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - PMS 项目新增/修改 Request VO") +@Data +public class PmsProjectSaveReqVO { + + @Schema(description = "项目编号", example = "1024") + private Long id; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "官网重构") + @NotBlank(message = "项目名称不能为空") + @Size(max = 31, message = "项目名称不能超过 31 个字符") + private String name; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "项目类型不能为空") + @InEnum(PmsProjectTypeEnum.class) + private Integer type; + + @Schema(description = "项目优先级", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "项目优先级不能为空") + @InEnum(PmsProjectLevelEnum.class) + private Integer level; + + @Schema(description = "项目描述", example = "完成官网前后端重构") + @Size(max = 500, message = "项目描述不能超过 500 个字符") + private String description; + + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "项目可见范围不能为空") + private Boolean openStatus; + + @Schema(description = "项目图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "ep:folder") + @NotBlank(message = "项目图标不能为空") + private String icon; + + @Schema(description = "开始时间", example = "2026-08-01 00:00:00") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime startTime; + + @Schema(description = "截止时间", example = "2026-12-31 23:59:59") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endTime; + + @Schema(description = "初始成员编号列表", example = "[1, 2]") + private List memberUserIds; + + @JsonIgnore + @Schema(hidden = true) + @AssertTrue(message = "项目截止时间必须晚于开始时间,且开始、截止时间需要同时填写") + public boolean isTimeValid() { + return startTime == null && endTime == null + || startTime != null && endTime != null && startTime.isBefore(endTime); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplatePageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplatePageReqVO.java new file mode 100644 index 000000000..e754e6bb6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplatePageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - PMS 项目模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PmsProjectTemplatePageReqVO extends PageParam { + + @Schema(description = "模板名称,模糊匹配", example = "敏捷研发模板") + private String name; + + @Schema(description = "项目类型", example = "2") + @InEnum(PmsProjectTypeEnum.class) + private Integer projectType; + + @Schema(description = "模板状态", example = "0") + private Integer status; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplateRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplateRespVO.java new file mode 100644 index 000000000..6f647ac74 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplateRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 项目模板 Response VO") +@Data +public class PmsProjectTemplateRespVO { + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏捷研发模板") + private String name; + + @Schema(description = "模板描述", example = "适用于标准敏捷研发项目") + private String description; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer projectType; + + @Schema(description = "模板状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer sort; + + @Schema(description = "启用的工作项类型列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List itemTypes; + + @Schema(description = "工作项状态模板列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List statuses; + + @Schema(description = "看板列模板列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List boards; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplateSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplateSaveReqVO.java new file mode 100644 index 000000000..4202291c8 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/project/vo/template/PmsProjectTemplateSaveReqVO.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 项目模板新增/修改 Request VO") +@Data +public class PmsProjectTemplateSaveReqVO { + + @Schema(description = "模板编号", example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏捷研发模板") + @NotBlank(message = "模板名称不能为空") + @Size(max = 100, message = "模板名称不能超过 100 个字符") + private String name; + + @Schema(description = "模板描述", example = "适用于标准敏捷研发项目") + @Size(max = 500, message = "模板描述不能超过 500 个字符") + private String description; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "项目类型不能为空") + @InEnum(PmsProjectTypeEnum.class) + private Integer projectType; + + @Schema(description = "模板状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "模板状态不能为空") + private Integer status; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "启用的工作项类型列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2, 3, 4]") + @NotEmpty(message = "工作项类型不能为空") + private List itemTypes; + + @Schema(description = "工作项状态模板列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "工作项状态不能为空") + private List statuses; + + @Schema(description = "看板列模板列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotEmpty(message = "看板列不能为空") + private List boards; + + @Schema(description = "工作项状态模板") + @Data + public static class StatusTemplate { + + @Schema(description = "模板内稳定编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "task_todo") + @NotBlank(message = "状态编码不能为空") + @Size(max = 50, message = "状态编码不能超过 50 个字符") + private String code; + + @Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "待处理") + @NotBlank(message = "状态名称不能为空") + @Size(max = 50, message = "状态名称不能超过 50 个字符") + private String name; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "状态工作项类型不能为空") + @InEnum(PmsWorkItemTypeEnum.class) + private Integer workItemType; + + @Schema(description = "语义状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "语义状态不能为空") + @InEnum(PmsWorkItemStatusTypeEnum.class) + private Integer statusType; + + @Schema(description = "是否为初始状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否为初始状态不能为空") + private Boolean defaultStatus; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "状态显示顺序不能为空") + private Integer sort; + + @Schema(description = "所属看板列编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "todo") + @NotBlank(message = "状态所属看板列不能为空") + private String boardCode; + } + + @Schema(description = "看板列模板") + @Data + public static class BoardTemplate { + + @Schema(description = "模板内稳定编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "todo") + @NotBlank(message = "看板列编码不能为空") + @Size(max = 50, message = "看板列编码不能超过 50 个字符") + private String code; + + @Schema(description = "看板列名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "待处理") + @NotBlank(message = "看板列名称不能为空") + @Size(max = 50, message = "看板列名称不能超过 50 个字符") + private String name; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "看板列工作项类型不能为空") + @InEnum(PmsWorkItemTypeEnum.class) + private Integer workItemType; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "看板列显示顺序不能为空") + private Integer sort; + + @Schema(description = "关联的状态编码列表", requiredMode = Schema.RequiredMode.REQUIRED, + example = "[\"task_todo\"]") + @NotEmpty(message = "看板列关联状态不能为空") + private List statusCodes; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/PmsWorkbenchController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/PmsWorkbenchController.java new file mode 100644 index 000000000..eca2f134a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/PmsWorkbenchController.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workbench; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchCountRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchIterationRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchWorkItemRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectService; +import cn.iocoder.yudao.module.pms.service.pm.workbench.PmsWorkbenchService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemStatusService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 工作台") +@RestController +@RequestMapping("/pms/pm/workbench") +@Validated +public class PmsWorkbenchController { + + @Resource + private PmsWorkbenchService workbenchService; + @Resource + private PmsProjectService projectService; + @Resource + private PmsProjectMemberService projectMemberService; + @Resource + private PmsWorkItemStatusService workItemStatusService; + @Resource + private PermissionApi permissionApi; + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/work-item-page") + @Operation(summary = "获得我负责的工作项分页") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult> getWorkbenchWorkItemPage( + @Valid PmsWorkbenchPageReqVO pageReqVO) { + // 1. 查询当前用户负责的工作项分页 + Long userId = getLoginUserId(); + PageResult pageResult = workbenchService.getWorkbenchWorkItemPage(pageReqVO, userId); + // 2.1 批量查询项目、状态及项目写权限 + Map projectMap = projectService.getProjectMap( + convertSet(pageResult.getList(), PmsWorkItemDO::getProjectId)); + Map statusMap = workItemStatusService.getWorkItemStatusMap( + convertSet(pageResult.getList(), PmsWorkItemDO::getStatusId)); + Set writableProjectIds = convertSet(projectMemberService.getWritableProjectIdListByUserId(userId)); + boolean superAdmin = permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData(); + // 2.2 批量查询负责人和创建人 + Set userIds = convertSet(pageResult.getList(), PmsWorkItemDO::getAssigneeUserId); + userIds.addAll(convertSet(pageResult.getList(), workItem -> NumberUtils.parseLong(workItem.getCreator()))); + Map userMap = adminUserApi.getUserMap(userIds); + Map creatorUserIdMap = convertMap(pageResult.getList(), PmsWorkItemDO::getId, + workItem -> NumberUtils.parseLong(workItem.getCreator())); + // 3. 转换响应 VO,并拼接项目、状态、用户及写权限 + List workItemVOList = BeanUtils.toBean(pageResult.getList(), + PmsWorkbenchWorkItemRespVO.class, itemVO -> { + findAndThen(projectMap, itemVO.getProjectId(), project -> itemVO + .setProjectName(project.getName()).setProjectType(project.getType())); + findAndThen(statusMap, itemVO.getStatusId(), status -> itemVO.setStatusName(status.getName())); + findAndThen(userMap, itemVO.getAssigneeUserId(), + user -> itemVO.setAssigneeUserName(user.getNickname())); + itemVO.setCreatorUserId(creatorUserIdMap.get(itemVO.getId())); + findAndThen(userMap, itemVO.getCreatorUserId(), + user -> itemVO.setCreatorUserName(user.getNickname())); + itemVO.setWriteStatus(superAdmin || writableProjectIds.contains(itemVO.getProjectId())); + }); + return success(new PageResult<>(workItemVOList, pageResult.getTotal())); + } + + @GetMapping("/iteration-page") + @Operation(summary = "获得我负责的迭代分页") + @PreAuthorize("@ss.hasPermission('pms:pm:iteration:query')") + public CommonResult> getWorkbenchIterationPage( + @Valid PmsWorkbenchPageReqVO pageReqVO) { + // 1.1 查询当前用户负责的迭代分页 + PageResult pageResult = workbenchService.getWorkbenchIterationPage(pageReqVO, getLoginUserId()); + // 1.2 批量查询关联项目 + Map projectMap = projectService.getProjectMap( + convertSet(pageResult.getList(), PmsIterationDO::getProjectId)); + // 2. 转换响应 VO,并拼接项目展示字段 + List iterationVOList = BeanUtils.toBean(pageResult.getList(), + PmsWorkbenchIterationRespVO.class, itemVO -> + findAndThen(projectMap, itemVO.getProjectId(), project -> itemVO + .setProjectName(project.getName()).setProjectType(project.getType()))); + return success(new PageResult<>(iterationVOList, pageResult.getTotal())); + } + + @GetMapping("/count") + @Operation(summary = "获得我的未完成事项数量") + @PreAuthorize("@ss.hasAnyPermissions('pms:pm:work-item:query', 'pms:pm:iteration:query')") + public CommonResult getWorkbenchCount(PmsWorkbenchPageReqVO pageReqVO) { + return success(workbenchService.getWorkbenchCount(pageReqVO, getLoginUserId())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchCountRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchCountRespVO.java new file mode 100644 index 000000000..8ed30b29e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchCountRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +@Schema(description = "管理后台 - PMS 工作台数量响应 VO") +@Data +@Accessors(chain = true) +public class PmsWorkbenchCountRespVO { + + @Schema(description = "需求数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer requirementCount; + + @Schema(description = "任务数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer taskCount; + + @Schema(description = "缺陷数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer defectCount; + + @Schema(description = "迭代数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer iterationCount; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchIterationRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchIterationRespVO.java new file mode 100644 index 000000000..d6453ee64 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchIterationRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 工作台迭代 Response VO") +@Data +public class PmsWorkbenchIterationRespVO { + + @Schema(description = "迭代编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long projectId; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "商城项目") + private String projectName; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer projectType; + + @Schema(description = "迭代名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "第一个迭代") + private String name; + + @Schema(description = "迭代状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer status; + + @Schema(description = "开始时间", example = "2026-01-01 00:00:00") + private LocalDateTime startTime; + + @Schema(description = "结束时间", example = "2026-01-31 23:59:59") + private LocalDateTime endTime; + + @Schema(description = "迭代目标", example = "完成登录模块") + private String target; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchPageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchPageReqVO.java new file mode 100644 index 000000000..b213fc714 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchPageReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - PMS 工作台分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkbenchPageReqVO extends PageParam { + + @Schema(description = "项目编号", example = "1024") + private Long projectId; + + @Schema(description = "工作项类型", example = "3") + @InEnum(PmsWorkItemTypeEnum.class) + private Integer type; + + @Schema(description = "名称", example = "登录") + private String name; + + @Schema(description = "状态,工作项为语义状态,迭代为迭代状态", example = "2") + private Integer status; + + @Schema(description = "优先级", example = "2") + @InEnum(PmsWorkItemPriorityEnum.class) + private Integer priority; + + @Schema(description = "迭代编号", example = "1024") + private Long iterationId; + + @Schema(description = "截止时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] endTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchWorkItemRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchWorkItemRespVO.java new file mode 100644 index 000000000..ed0ae1dda --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workbench/vo/PmsWorkbenchWorkItemRespVO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 工作台工作项 Response VO") +@Data +public class PmsWorkbenchWorkItemRespVO { + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long projectId; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "商城项目") + private String projectName; + + @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer projectType; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer type; + + @Schema(description = "工作项序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer serialNumber; + + @Schema(description = "工作项名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "优化登录页") + private String name; + + @Schema(description = "优先级", example = "2") + private Integer priority; + + @Schema(description = "负责人用户编号", example = "1024") + private Long assigneeUserId; + + @Schema(description = "负责人姓名", example = "芋道源码") + private String assigneeUserName; + + @Schema(description = "创建人用户编号", example = "2048") + private Long creatorUserId; + + @Schema(description = "创建人姓名", example = "源码") + private String creatorUserName; + + @Schema(description = "状态编号", example = "1") + private Long statusId; + + @Schema(description = "状态名称", example = "进行中") + private String statusName; + + @Schema(description = "语义状态", example = "2") + private Integer status; + + @Schema(description = "进度", example = "60") + private Integer progress; + + @Schema(description = "开始时间", example = "2026-01-01 10:00:00") + private LocalDateTime startTime; + + @Schema(description = "结束时间", example = "2026-01-31 18:00:00") + private LocalDateTime endTime; + + @Schema(description = "是否可编辑", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean writeStatus; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemActivityController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemActivityController.java new file mode 100644 index 000000000..f9508dfb4 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemActivityController.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.activity.PmsWorkItemActivityRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemActivityDO; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemActivityService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +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 java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 工作项动态") +@RestController +@RequestMapping("/pms/pm/work-item-activity") +@Validated +public class PmsWorkItemActivityController { + + @Resource + private PmsWorkItemActivityService activityService; + @Resource + private PmsWorkItemService workItemService; + + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/list") + @Operation(summary = "获得工作项动态列表") + @Parameter(name = "workItemId", description = "工作项编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult> getWorkItemActivityList( + @RequestParam("workItemId") Long workItemId) { + workItemService.getWorkItem(workItemId, getLoginUserId()); + List activities = activityService.getWorkItemActivityList(workItemId); + // 拼接 VO:批量查询操作人,并补充用户展示字段 + Map userMap = adminUserApi.getUserMap( + convertSet(activities, PmsWorkItemActivityDO::getOperatorUserId)); + List activityVOList = BeanUtils.toBean( + activities, PmsWorkItemActivityRespVO.class, activityVO -> + findAndThen(userMap, activityVO.getOperatorUserId(), user -> activityVO + .setOperatorUserName(user.getNickname()).setOperatorUserAvatar(user.getAvatar()))); + return success(activityVOList); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemCommentController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemCommentController.java new file mode 100644 index 000000000..d741003fd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemCommentController.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.comment.PmsWorkItemCommentRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.comment.PmsWorkItemCommentSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemCommentDO; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemCommentService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 工作项评论") +@RestController +@RequestMapping("/pms/pm/work-item-comment") +@Validated +public class PmsWorkItemCommentController { + + @Resource + private PmsWorkItemCommentService commentService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建工作项评论") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult createWorkItemComment(@Valid @RequestBody PmsWorkItemCommentSaveReqVO saveReqVO) { + return success(commentService.createWorkItemComment(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "修改工作项评论") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemComment(@Valid @RequestBody PmsWorkItemCommentSaveReqVO saveReqVO) { + commentService.updateWorkItemComment(saveReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工作项评论") + @Parameter(name = "id", description = "评论编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult deleteWorkItemComment(@RequestParam("id") Long id) { + commentService.deleteWorkItemComment(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得工作项评论列表") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult> getWorkItemCommentList( + @RequestParam("workItemId") Long workItemId) { + List comments = commentService.getWorkItemCommentList(workItemId, getLoginUserId()); + // 拼接 VO:批量查询评论人和回复对象 + Set userIds = convertSet(comments, PmsWorkItemCommentDO::getUserId); + userIds.addAll(convertSet(comments, PmsWorkItemCommentDO::getReplyUserId)); + Map userMap = adminUserApi.getUserMap(userIds); + // 拼接 VO:转换评论,并按主评论组织回复列表 + Map mainCommentMap = new LinkedHashMap<>(); + List mainComments = new ArrayList<>(); + for (PmsWorkItemCommentDO comment : comments) { + PmsWorkItemCommentRespVO commentVO = BeanUtils.toBean(comment, PmsWorkItemCommentRespVO.class).setChildren(new ArrayList<>()); + findAndThen(userMap, comment.getUserId(), user -> commentVO.setUserName(user.getNickname())); + findAndThen(userMap, comment.getReplyUserId(), user -> commentVO.setReplyUserName(user.getNickname())); + if (PmsWorkItemCommentDO.MAIN_ID_ROOT.equals(comment.getMainId())) { + mainCommentMap.put(comment.getId(), commentVO); + mainComments.add(commentVO); + } else { + PmsWorkItemCommentRespVO mainComment = mainCommentMap.get(comment.getMainId()); + if (mainComment != null) { + mainComment.getChildren().add(commentVO); + } + } + } + return success(mainComments); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemController.java new file mode 100644 index 000000000..48d0dc12c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemController.java @@ -0,0 +1,299 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label.PmsWorkItemLabelRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemBoardReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemBoardRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemImportExcelVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemImportRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemIterationUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemNameUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPlanningSortReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemLabelService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemStatusService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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.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 org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 工作项") +@RestController +@RequestMapping("/pms/pm/work-item") +@Validated +public class PmsWorkItemController { + + @Resource + private PmsWorkItemService workItemService; + @Resource + private PmsWorkItemLabelService workItemLabelService; + @Resource + private PmsWorkItemStatusService workItemStatusService; + @Resource + private PmsIterationService iterationService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建工作项") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:create')") + public CommonResult createWorkItem(@Valid @RequestBody PmsWorkItemSaveReqVO saveReqVO) { + return success(workItemService.createWorkItem(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新工作项") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItem(@Valid @RequestBody PmsWorkItemSaveReqVO saveReqVO) { + workItemService.updateWorkItem(saveReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-name") + @Operation(summary = "更新工作项名称") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemName(@Valid @RequestBody PmsWorkItemNameUpdateReqVO updateReqVO) { + workItemService.updateWorkItemName(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新工作项状态") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemStatus(@Valid @RequestBody PmsWorkItemStatusUpdateReqVO updateReqVO) { + workItemService.updateWorkItemStatus(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-iteration") + @Operation(summary = "更新工作项所属迭代") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemIteration(@Valid @RequestBody PmsWorkItemIterationUpdateReqVO updateReqVO) { + workItemService.updateWorkItemIteration(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-sort") + @Operation(summary = "更新看板列内工作项顺序") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemSort(@Valid @RequestBody PmsWorkItemSortReqVO sortReqVO) { + workItemService.updateWorkItemSort(sortReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-planning-sort") + @Operation(summary = "更新待规划或迭代内工作项顺序") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemPlanningSort(@Valid @RequestBody PmsWorkItemPlanningSortReqVO sortReqVO) { + workItemService.updateWorkItemPlanningSort(sortReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/archive") + @Operation(summary = "归档工作项") + @Parameter(name = "id", description = "工作项编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult archiveWorkItem(@RequestParam("id") Long id) { + workItemService.archiveWorkItem(id, getLoginUserId()); + return success(true); + } + + @PutMapping("/recycle") + @Operation(summary = "将工作项移入回收站") + @Parameter(name = "id", description = "工作项编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult recycleWorkItem(@RequestParam("id") Long id) { + workItemService.recycleWorkItem(id, getLoginUserId()); + return success(true); + } + + @PutMapping("/restore") + @Operation(summary = "恢复工作项") + @Parameter(name = "id", description = "工作项编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult restoreWorkItem(@RequestParam("id") Long id) { + workItemService.restoreWorkItem(id, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "彻底删除回收站中的工作项") + @Parameter(name = "id", description = "工作项编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:delete')") + public CommonResult deleteWorkItem(@RequestParam("id") Long id) { + workItemService.deleteWorkItem(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得工作项详情") + @Parameter(name = "id", description = "工作项编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult getWorkItem(@RequestParam("id") Long id) { + PmsWorkItemDO workItem = workItemService.getWorkItem(id, getLoginUserId()); + PmsWorkItemRespVO workItemVO = buildWorkItemRespVO(workItem); + return success(workItemVO); + } + + @GetMapping("/page") + @Operation(summary = "获得工作项分页") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult> getWorkItemPage(@Valid PmsWorkItemPageReqVO pageReqVO) { + PageResult pageResult = workItemService.getWorkItemPage(pageReqVO, getLoginUserId()); + return success(new PageResult<>(buildWorkItemRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出工作项") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:export')") + public void exportWorkItemList(@Valid PmsWorkItemPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = workItemService.getWorkItemList(pageReqVO, getLoginUserId()); + List workItemVOList = buildWorkItemRespVOList(list); + ExcelUtils.write(response, "工作项.xlsx", "工作项", PmsWorkItemRespVO.class, workItemVOList); + } + + @GetMapping("/get-import-template") + @Operation(summary = "下载工作项导入模板") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:import')") + public void getWorkItemImportTemplate(HttpServletResponse response) throws IOException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime deadline = now.plusDays(3); + PmsWorkItemImportExcelVO example = PmsWorkItemImportExcelVO.builder() + .name("登录页面开发").description("实现账号密码登录").statusName("") + .priority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()) + .startTime(now).endTime(deadline) + .labels(Collections.emptyList()).progress(0).estimatedHours(16) + .build(); + ExcelUtils.write(response, "工作项导入模板.xlsx", "工作项", PmsWorkItemImportExcelVO.class, + Collections.singletonList(example)); + } + + @PostMapping("/import") + @Operation(summary = "导入工作项") + @Parameters({ + @Parameter(name = "projectId", description = "项目编号", required = true, example = "1024"), + @Parameter(name = "type", description = "工作项类型(需求、任务、缺陷)", required = true, example = "1"), + @Parameter(name = "file", description = "Excel 文件", required = true) + }) + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:import')") + public CommonResult importWorkItemList( + @RequestParam("projectId") Long projectId, + @RequestParam("type") Integer workItemType, + @RequestParam("file") MultipartFile file) throws Exception { + return success(workItemService.importWorkItemList(projectId, workItemType, + ExcelUtils.read(file, PmsWorkItemImportExcelVO.class), getLoginUserId())); + } + + @GetMapping("/board") + @Operation(summary = "获得工作项看板") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult> getWorkItemBoard(@Valid PmsWorkItemBoardReqVO queryReqVO) { + List columns = workItemService.getWorkItemBoard(queryReqVO, getLoginUserId()); + fillWorkItemRespVOList(convertListByFlatMap(columns, column -> column.getItems().stream())); + return success(columns); + } + + // ==================== 拼接 VO ==================== + + private PmsWorkItemRespVO buildWorkItemRespVO(PmsWorkItemDO workItem) { + return CollUtil.getFirst(buildWorkItemRespVOList(Collections.singletonList(workItem))); + } + + private List buildWorkItemRespVOList(List workItems) { + if (CollUtil.isEmpty(workItems)) { + return Collections.emptyList(); + } + return fillWorkItemRespVOList(BeanUtils.toBean(workItems, PmsWorkItemRespVO.class)); + } + + private List fillWorkItemRespVOList(List workItems) { + if (CollUtil.isEmpty(workItems)) { + return Collections.emptyList(); + } + + // 1.1 批量查询状态、迭代、关联需求和参与人 + Map statusMap = workItemStatusService.getWorkItemStatusMap( + convertSet(workItems, PmsWorkItemRespVO::getStatusId)); + Map iterationMap = iterationService.getIterationMap( + convertSet(workItems, PmsWorkItemRespVO::getIterationId)); + Map requirementMap = workItemService.getWorkItemMap( + convertSet(workItems, PmsWorkItemRespVO::getRelatedRequirementId)); + Map> memberUserIdListMap = workItemService.getWorkItemMemberUserIdListMap( + convertSet(workItems, PmsWorkItemRespVO::getId)); + Map labelMap = workItemLabelService.getWorkItemLabelMap( + convertSetByFlatMap(workItems, PmsWorkItemRespVO::getLabelIds, List::stream)); + // 1.2 批量查询负责人和参与人用户 + Set userIds = convertSetByFlatMap(workItems, workItem -> java.util.stream.Stream.concat( + java.util.stream.Stream.of(workItem.getAssigneeUserId()), + memberUserIdListMap.getOrDefault(workItem.getId(), Collections.emptyList()).stream())); + Map userMap = adminUserApi.getUserMap(userIds); + // 2. 拼接工作项展示字段 + for (PmsWorkItemRespVO workItemVO : workItems) { + findAndThen(statusMap, workItemVO.getStatusId(), status -> workItemVO.setStatusName(status.getName())); + findAndThen(iterationMap, workItemVO.getIterationId(), + iteration -> workItemVO.setIterationName(iteration.getName())); + findAndThen(requirementMap, workItemVO.getRelatedRequirementId(), + requirement -> workItemVO.setRelatedRequirementName(requirement.getName())); + findAndThen(userMap, workItemVO.getAssigneeUserId(), + assignee -> workItemVO.setAssigneeUserName(assignee.getNickname())); + List memberUserIds = memberUserIdListMap.getOrDefault(workItemVO.getId(), Collections.emptyList()); + List memberUserNames = convertList(convertList(memberUserIds, userMap::get), + AdminUserRespDTO::getNickname); + workItemVO.setMemberUserIds(memberUserIds).setMemberUserNames(memberUserNames); + workItemVO.setLabels(BeanUtils.toBean(convertList(workItemVO.getLabelIds(), labelMap::get), + PmsWorkItemLabelRespVO.class)); + } + return workItems; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemLabelController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemLabelController.java new file mode 100644 index 000000000..2a0b24b64 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemLabelController.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label.PmsWorkItemLabelRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label.PmsWorkItemLabelSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemLabelService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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 cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - PMS 工作项标签") +@RestController +@RequestMapping("/pms/pm/work-item-label") +@Validated +public class PmsWorkItemLabelController { + + @Resource + private PmsWorkItemLabelService labelService; + + @PostMapping("/create") + @Operation(summary = "创建工作项标签") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult createWorkItemLabel(@Valid @RequestBody PmsWorkItemLabelSaveReqVO saveReqVO) { + return success(labelService.createWorkItemLabel(saveReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改工作项标签") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemLabel(@Valid @RequestBody PmsWorkItemLabelSaveReqVO saveReqVO) { + labelService.updateWorkItemLabel(saveReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工作项标签") + @Parameter(name = "id", description = "标签编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult deleteWorkItemLabel(@RequestParam("id") Long id) { + labelService.deleteWorkItemLabel(id); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得工作项标签列表") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult> getWorkItemLabelList( + @RequestParam(value = "name", required = false) String name) { + List labels = labelService.getWorkItemLabelList(name); + return success(BeanUtils.toBean(labels, PmsWorkItemLabelRespVO.class)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemStatusController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemStatusController.java new file mode 100644 index 000000000..3469acdfd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemStatusController.java @@ -0,0 +1,159 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemBoardConfigRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemBoardConfigSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusConfigUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusDeleteReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemStatusService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 工作项状态") +@RestController +@RequestMapping("/pms/pm/work-item-status") +@Validated +public class PmsWorkItemStatusController { + + @Resource + private PmsWorkItemStatusService workItemStatusService; + + @PostMapping("/create") + @Operation(summary = "创建工作项状态") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult createWorkItemStatus( + @Valid @RequestBody PmsWorkItemStatusCreateReqVO createReqVO) { + return success(workItemStatusService.createWorkItemStatus(createReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新工作项状态") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemStatusConfig( + @Valid @RequestBody PmsWorkItemStatusConfigUpdateReqVO updateReqVO) { + workItemStatusService.updateWorkItemStatusConfig(updateReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-default") + @Operation(summary = "更新工作项初始状态") + @Parameter(name = "id", description = "状态编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateDefaultWorkItemStatus(@RequestParam("id") Long id) { + workItemStatusService.updateDefaultWorkItemStatus(id, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-sort") + @Operation(summary = "更新工作项状态顺序") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemStatusSort( + @Valid @RequestBody PmsWorkItemStatusSortReqVO sortReqVO) { + workItemStatusService.updateWorkItemStatusSort(sortReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/update-board-config") + @Operation(summary = "更新工作项看板列和状态映射") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemBoardConfig( + @Valid @RequestBody PmsWorkItemBoardConfigSaveReqVO saveReqVO) { + workItemStatusService.updateWorkItemBoardConfig(saveReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工作项状态并迁移工作项") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult deleteWorkItemStatus( + @Valid @RequestBody PmsWorkItemStatusDeleteReqVO deleteReqVO) { + workItemStatusService.deleteWorkItemStatus(deleteReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得工作项状态详情") + @Parameter(name = "id", description = "状态编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult getWorkItemStatus(@RequestParam("id") Long id) { + PmsWorkItemStatusDO status = workItemStatusService.getWorkItemStatus(id, getLoginUserId()); + return success(BeanUtils.toBean(status, PmsWorkItemStatusRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得工作项看板状态列表") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult> getWorkItemStatusList( + @RequestParam("projectId") Long projectId, @RequestParam("type") Integer workItemType) { + List statuses = workItemStatusService.getWorkItemStatusList(projectId, workItemType, + getLoginUserId()); + return success(BeanUtils.toBean(statuses, PmsWorkItemStatusRespVO.class)); + } + + @GetMapping("/get-board-config") + @Operation(summary = "获得工作项看板列和状态映射") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult getWorkItemBoardConfig( + @RequestParam("projectId") Long projectId, @RequestParam("type") Integer workItemType) { + // 1. 校验项目可读并查询已保存的看板列、状态 + List statuses = workItemStatusService + .getWorkItemStatusList(projectId, workItemType, getLoginUserId()); + List boards = workItemStatusService.getWorkItemBoardList(projectId, workItemType); + + // 2.1 按看板列名称聚合状态编号 + Map> statusIdMap = new LinkedHashMap<>(); + for (PmsWorkItemStatusDO status : statuses) { + if (StrUtil.isNotBlank(status.getBoardName())) { + statusIdMap.computeIfAbsent(status.getBoardName(), key -> new ArrayList<>()).add(status.getId()); + } + } + // 2.2 按看板列顺序组装响应,并保留空列 + Set assignedStatusIds = new LinkedHashSet<>(); + List boardVOs = new ArrayList<>(); + for (PmsWorkItemBoardDO board : boards) { + List statusIds = statusIdMap.getOrDefault(board.getName(), new ArrayList<>()); + assignedStatusIds.addAll(statusIds); + boardVOs.add(new PmsWorkItemBoardConfigRespVO.Board().setId(board.getId()) + .setName(board.getName()).setStatusIds(statusIds)); + } + // 2.3 收集未分配到看板列的状态 + List unassignedStatusIds = new ArrayList<>(); + for (PmsWorkItemStatusDO status : statuses) { + if (!assignedStatusIds.contains(status.getId())) { + unassignedStatusIds.add(status.getId()); + } + } + return success(new PmsWorkItemBoardConfigRespVO().setBoards(boardVOs).setUnassignedStatusIds(unassignedStatusIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemWorkLogController.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemWorkLogController.java new file mode 100644 index 000000000..b44d1c0f3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/PmsWorkItemWorkLogController.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsProjectWorkLogReportReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsProjectWorkLogReportRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSummaryRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemWorkLogService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +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.GetMapping; +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.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - PMS 工作项工时") +@RestController +@RequestMapping("/pms/pm/work-item-work-log") +@Validated +public class PmsWorkItemWorkLogController { + + @Resource + private PmsWorkItemWorkLogService workLogService; + @Resource + private PmsWorkItemService workItemService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建工作项工时记录") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult createWorkItemWorkLog(@Valid @RequestBody PmsWorkItemWorkLogSaveReqVO saveReqVO) { + return success(workLogService.createWorkItemWorkLog(saveReqVO, getLoginUserId())); + } + + @PutMapping("/update") + @Operation(summary = "更新工作项工时记录") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:update')") + public CommonResult updateWorkItemWorkLog(@Valid @RequestBody PmsWorkItemWorkLogSaveReqVO saveReqVO) { + workLogService.updateWorkItemWorkLog(saveReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得工作项工时记录") + @Parameter(name = "id", description = "工时记录编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult getWorkItemWorkLog(@RequestParam("id") Long id) { + PmsWorkItemWorkLogDO workLog = workLogService.getWorkItemWorkLog(id, getLoginUserId()); + PmsWorkItemWorkLogRespVO respVO = BeanUtils.toBean(workLog, PmsWorkItemWorkLogRespVO.class) + .setCreatorUserId(NumberUtils.parseLong(workLog.getCreator())); + fillWorkItemWorkLogRespVOList(Collections.singletonList(respVO)); + return success(respVO); + } + + @GetMapping("/summary") + @Operation(summary = "获得工作项工时汇总") + @Parameter(name = "workItemId", description = "工作项编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult getWorkItemWorkLogSummary( + @RequestParam("workItemId") Long workItemId) { + PmsWorkItemWorkLogSummaryRespVO summary = workLogService.getWorkItemWorkLogSummary(workItemId, getLoginUserId()); + fillWorkItemWorkLogRespVOList(summary.getRecords()); + return success(summary); + } + + @GetMapping("/project-report") + @Operation(summary = "获得项目工时报表") + @PreAuthorize("@ss.hasPermission('pms:pm:work-item:query')") + public CommonResult getProjectWorkItemWorkLogReport( + @Valid PmsProjectWorkLogReportReqVO reqVO) { + PmsProjectWorkLogReportRespVO report = workLogService.getProjectWorkItemWorkLogReport( + reqVO, getLoginUserId()); + return success(report); + } + + // ==================== 拼接 VO ==================== + + private List fillWorkItemWorkLogRespVOList(List workLogs) { + if (CollUtil.isEmpty(workLogs)) { + return Collections.emptyList(); + } + // 1. 批量查询创建人和工作项 + Map userMap = adminUserApi.getUserMap(convertSet( + workLogs, PmsWorkItemWorkLogRespVO::getCreatorUserId)); + Map workItemMap = workItemService.getWorkItemMap( + convertSet(workLogs, PmsWorkItemWorkLogRespVO::getWorkItemId)); + // 2. 拼接创建人和工作项名称 + for (PmsWorkItemWorkLogRespVO workLog : workLogs) { + findAndThen(userMap, workLog.getCreatorUserId(), user -> workLog.setCreatorUserName(user.getNickname())); + findAndThen(workItemMap, workLog.getWorkItemId(), item -> workLog.setWorkItemName(item.getName())); + } + return workLogs; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/activity/PmsWorkItemActivityRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/activity/PmsWorkItemActivityRespVO.java new file mode 100644 index 000000000..ac4546fba --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/activity/PmsWorkItemActivityRespVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 工作项动态响应 VO") +@Data +@Accessors(chain = true) +public class PmsWorkItemActivityRespVO { + + @Schema(description = "动态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long workItemId; + + @Schema(description = "操作人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long operatorUserId; + + @Schema(description = "操作人姓名", example = "芋道") + private String operatorUserName; + + @Schema(description = "操作人头像") + private String operatorUserAvatar; + + @Schema(description = "动态内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "更新了工作项") + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/comment/PmsWorkItemCommentRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/comment/PmsWorkItemCommentRespVO.java new file mode 100644 index 000000000..956eea13f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/comment/PmsWorkItemCommentRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.comment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 工作项评论 Response VO") +@Data +public class PmsWorkItemCommentRespVO { + + @Schema(description = "评论编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long workItemId; + + @Schema(description = "评论人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "评论人姓名", example = "芋道源码") + private String userName; + + @Schema(description = "主评论编号", example = "1024") + private Long mainId; + + @Schema(description = "回复对象用户编号", example = "2048") + private Long replyUserId; + + @Schema(description = "回复对象姓名", example = "源码") + private String replyUserName; + + @Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String content; + + @Schema(description = "回复列表") + private List children; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/comment/PmsWorkItemCommentSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/comment/PmsWorkItemCommentSaveReqVO.java new file mode 100644 index 000000000..2832b4848 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/comment/PmsWorkItemCommentSaveReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.comment; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 工作项评论新增/修改 Request VO") +@Data +public class PmsWorkItemCommentSaveReqVO { + + @Schema(description = "评论编号", example = "1024") + private Long id; + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "工作项编号不能为空") + private Long workItemId; + + @Schema(description = "主评论编号,回复时传入", example = "1024") + private Long mainId; + + @Schema(description = "回复对象用户编号", example = "2048") + private Long replyUserId; + + @Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "已处理") + @NotBlank(message = "评论内容不能为空") + @Size(max = 2000, message = "评论内容不能超过 2000 个字符") + private String content; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/label/PmsWorkItemLabelRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/label/PmsWorkItemLabelRespVO.java new file mode 100644 index 000000000..73fb78f00 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/label/PmsWorkItemLabelRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 工作项标签 Response VO") +@Data +public class PmsWorkItemLabelRespVO { + + @Schema(description = "标签编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "紧急") + private String name; + + @Schema(description = "标签颜色", requiredMode = Schema.RequiredMode.REQUIRED, example = "#409EFF") + private String color; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/label/PmsWorkItemLabelSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/label/PmsWorkItemLabelSaveReqVO.java new file mode 100644 index 000000000..326afbfaa --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/label/PmsWorkItemLabelSaveReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 工作项标签新增/修改 Request VO") +@Data +public class PmsWorkItemLabelSaveReqVO { + + @Schema(description = "标签编号", example = "1024") + private Long id; + + @Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "紧急") + @NotBlank(message = "标签名称不能为空") + @Size(max = 50, message = "标签名称不能超过 50 个字符") + private String name; + + @Schema(description = "标签颜色", requiredMode = Schema.RequiredMode.REQUIRED, example = "#409EFF") + @NotBlank(message = "标签颜色不能为空") + @Size(max = 20, message = "标签颜色不能超过 20 个字符") + private String color; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemBoardConfigRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemBoardConfigRespVO.java new file mode 100644 index 000000000..2dd65fa89 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemBoardConfigRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 工作项看板配置 Response VO") +@Data +public class PmsWorkItemBoardConfigRespVO { + + @Schema(description = "看板列列表") + private List boards; + + @Schema(description = "未放入看板的状态编号列表") + private List unassignedStatusIds; + + @Data + public static class Board { + + @Schema(description = "看板列编号") + private Long id; + + @Schema(description = "看板列名称") + private String name; + + @Schema(description = "关联状态编号列表") + private List statusIds; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemBoardConfigSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemBoardConfigSaveReqVO.java new file mode 100644 index 000000000..96baa402d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemBoardConfigSaveReqVO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +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.util.List; + +@Schema(description = "管理后台 - PMS 工作项看板配置保存 Request VO") +@Data +public class PmsWorkItemBoardConfigSaveReqVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "工作项类型不能为空") + @InEnum(PmsWorkItemTypeEnum.class) + private Integer workItemType; + + @Schema(description = "看板列列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "看板列列表不能为空") + private List boards; + + @Schema(description = "管理后台 - PMS 工作项看板列") + @Data + public static class Board { + + @Schema(description = "看板列编号", example = "1024") + private Long id; + + @Schema(description = "看板列名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "进行中") + @NotBlank(message = "看板列名称不能为空") + @Size(max = 50, message = "看板列名称不能超过 50 个字符") + private String name; + + @Schema(description = "关联状态编号列表,可为空", example = "[1024, 2048]") + @NotNull(message = "关联状态编号列表不能为空") + private List statusIds; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusConfigUpdateReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusConfigUpdateReqVO.java new file mode 100644 index 000000000..5634c54f5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusConfigUpdateReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 更新工作项状态配置 Request VO") +@Data +public class PmsWorkItemStatusConfigUpdateReqVO { + + @Schema(description = "状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "状态编号不能为空") + private Long id; + + @Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "待验收") + @NotBlank(message = "状态名称不能为空") + @Size(max = 50, message = "状态名称不能超过 50 个字符") + private String name; + + @Schema(description = "语义状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "语义状态不能为空") + @InEnum(PmsWorkItemStatusTypeEnum.class) + private Integer statusType; + + @Schema(description = "状态描述", example = "需求已拆分,等待开发") + @Size(max = 255, message = "状态描述不能超过 255 个字符") + private String description; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusCreateReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusCreateReqVO.java new file mode 100644 index 000000000..49a7e7b2c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusCreateReqVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 创建工作项状态 Request VO") +@Data +public class PmsWorkItemStatusCreateReqVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目不能为空") + private Long projectId; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "工作项类型不能为空") + @InEnum(PmsWorkItemTypeEnum.class) + private Integer workItemType; + + @Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "待验收") + @NotBlank(message = "状态名称不能为空") + @Size(max = 50, message = "状态名称不能超过 50 个字符") + private String name; + + @Schema(description = "语义状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "语义状态不能为空") + @InEnum(PmsWorkItemStatusTypeEnum.class) + private Integer statusType; + + @Schema(description = "状态描述", example = "需求已拆分,等待开发") + @Size(max = 255, message = "状态描述不能超过 255 个字符") + private String description; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusDeleteReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusDeleteReqVO.java new file mode 100644 index 000000000..58318a8f2 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusDeleteReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 删除工作项状态 Request VO") +@Data +public class PmsWorkItemStatusDeleteReqVO { + + @Schema(description = "待删除状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "待删除状态不能为空") + private Long id; + + @Schema(description = "工作项迁入状态编号;待删除状态存在工作项时必填", example = "2048") + private Long transferStatusId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusRespVO.java new file mode 100644 index 000000000..94be53a8c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 工作项看板状态 Response VO") +@Data +public class PmsWorkItemStatusRespVO { + + @Schema(description = "状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long projectId; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer workItemType; + + @Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "未开始") + private String name; + + @Schema(description = "语义状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer statusType; + + @Schema(description = "状态描述", example = "需求已拆分,等待开发") + private String description; + + @Schema(description = "所属看板列名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "处理中") + private String boardName; + + @Schema(description = "是否初始状态", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean defaultStatus; + + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sort; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusSortReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusSortReqVO.java new file mode 100644 index 000000000..62aa73d92 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusSortReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status; + +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 = "管理后台 - PMS 工作项状态排序 Request VO") +@Data +public class PmsWorkItemStatusSortReqVO { + + @Schema(description = "状态编号列表,顺序即显示顺序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "状态编号列表不能为空") + private List<@NotNull(message = "状态编号不能为空") Long> statusIds; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusUpdateReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusUpdateReqVO.java new file mode 100644 index 000000000..916932a94 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/status/PmsWorkItemStatusUpdateReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 更新工作项看板状态 Request VO") +@Data +public class PmsWorkItemStatusUpdateReqVO { + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "工作项编号不能为空") + private Long id; + + @Schema(description = "目标看板状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "目标看板状态不能为空") + private Long statusId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemBoardReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemBoardReqVO.java new file mode 100644 index 000000000..83f65d2d1 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemBoardReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - PMS 工作项看板 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemBoardReqVO extends PmsWorkItemPageReqVO { + + @Override + @NotNull(message = "工作项类型不能为空") + public Integer getType() { + return super.getType(); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemBoardRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemBoardRespVO.java new file mode 100644 index 000000000..cf89c1dd9 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemBoardRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 工作项看板列 Response VO") +@Data +public class PmsWorkItemBoardRespVO { + + @Schema(description = "看板列编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "看板列名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "映射到当前列的状态列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List statuses; + + @Schema(description = "工作项列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List items; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemImportExcelVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemImportExcelVO.java new file mode 100644 index 000000000..faff64860 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemImportExcelVO.java @@ -0,0 +1,121 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.framework.excel.core.convert.StringListConvert; +import cn.iocoder.yudao.module.pms.enums.DictTypeConstants; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * PMS 工作项 Excel 导入 VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - PMS 工作项 Excel 导入 VO") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ExcelIgnoreUnannotated +public class PmsWorkItemImportExcelVO { + + @ExcelProperty("事项标题") + @Schema(description = "工作项标题", example = "完成登录页") + private String name; + + @ExcelProperty("描述") + @Schema(description = "工作项描述", example = "实现账号密码登录") + private String description; + + @ExcelProperty("处理人用户编号") + @Schema(description = "处理人用户编号,为空时不指定处理人", example = "1024") + private Long assigneeUserId; + + @ExcelProperty("状态") + @Schema(description = "项目状态名称;为空时使用项目默认状态", example = "待处理") + private String statusName; + + @ExcelProperty(value = "优先级", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PMS_WORK_ITEM_PRIORITY) + @ExcelColumnSelect(dictType = DictTypeConstants.PMS_WORK_ITEM_PRIORITY) + @Schema(description = "优先级,Excel 中填写字典标签", example = "中") + private Integer priority; + + @ExcelProperty("开始时间") + @Schema(description = "开始时间", example = "2026-08-30 09:00:00") + private LocalDateTime startTime; + + @ExcelProperty("截止时间") + @Schema(description = "截止时间", example = "2026-09-01 18:00:00") + private LocalDateTime endTime; + + @ExcelProperty(value = "标签", converter = StringListConvert.class) + @Schema(description = "标签名称列表,多个标签可用逗号、顿号或斜杠分隔", example = "前端/登录") + private List labels; + + @ExcelProperty("进度") + @Schema(description = "完成进度,范围 0-100", example = "50") + private Integer progress; + + @ExcelProperty("预估工时") + @Schema(description = "预估工时,单位:小时", example = "8") + private Integer estimatedHours; + + @ExcelProperty("实际投入时长") + @Schema(description = "实际投入时长,单位:小时", example = "4") + @Min(value = 1, message = "实际投入时长必须大于 0") + private Integer actualHours; + + @ExcelProperty("剩余工时") + @Schema(description = "剩余工时,单位:小时", example = "4") + @Min(value = 0, message = "剩余工时不能小于 0") + private Integer remainingHours; + + @ExcelProperty(value = "缺陷类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PMS_WORK_ITEM_DEFECT_TYPE) + @ExcelColumnSelect(dictType = DictTypeConstants.PMS_WORK_ITEM_DEFECT_TYPE) + @Schema(description = "缺陷类型,Excel 中填写字典标签,仅缺陷工作项使用", example = "功能缺陷") + private Integer defectType; + + /** + * 导入时从 {@link #statusName} 解析出的项目状态编号,不作为 Excel 列 + */ + @Schema(hidden = true) + private Long statusId; + + /** + * 导入时从 {@link #labels} 解析出的标签编号列表,不作为 Excel 列 + */ + @Schema(hidden = true) + private List labelIds; + + /** + * 导入时使用的工作项类型,不作为 Excel 列 + */ + @Schema(hidden = true) + private Integer workItemType; + + @AssertTrue(message = "填写剩余工时时必须同时填写实际投入时长") + public boolean isWorkLogHoursValid() { + return actualHours != null || remainingHours == null; + } + + @AssertTrue(message = "缺陷类型仅可用于缺陷工作项") + public boolean isDefectTypeValid() { + return defectType == null || PmsWorkItemTypeEnum.DEFECT.getType().equals(workItemType); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemImportRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemImportRespVO.java new file mode 100644 index 000000000..6381833d5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemImportRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Schema(description = "管理后台 - PMS 工作项导入 Response VO") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PmsWorkItemImportRespVO { + + @Schema(description = "导入成功数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer successCount; + + @Schema(description = "导入失败行及原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureReasons; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemIterationUpdateReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemIterationUpdateReqVO.java new file mode 100644 index 000000000..58ee23b03 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemIterationUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.experimental.Accessors; + +@Schema(description = "管理后台 - PMS 工作项所属迭代更新 Request VO") +@Data +@Accessors(chain = true) +public class PmsWorkItemIterationUpdateReqVO { + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "工作项编号不能为空") + private Long id; + + @Schema(description = "迭代编号;为空表示移回待规划", example = "2048") + private Long iterationId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemNameUpdateReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemNameUpdateReqVO.java new file mode 100644 index 000000000..5963bdb10 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemNameUpdateReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 工作项名称更新 Request VO") +@Data +public class PmsWorkItemNameUpdateReqVO { + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "工作项编号不能为空") + private Long id; + + @Schema(description = "工作项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "完成登录页") + @NotBlank(message = "工作项标题不能为空") + @Size(max = 100, message = "工作项标题不能超过 100 个字符") + private String name; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemPageReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemPageReqVO.java new file mode 100644 index 000000000..2248482aa --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemPageReqVO.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemLifecycleStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 工作项分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemPageReqVO extends PageParam { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "工作项类型", example = "3") + @InEnum(PmsWorkItemTypeEnum.class) + private Integer type; + + @Schema(description = "工作项类型列表", example = "[2, 3, 4]") + private List types; + + @Schema(description = "工作项标题", example = "登录") + private String name; + + @Schema(description = "语义状态", example = "1") + @InEnum(PmsWorkItemStatusTypeEnum.class) + private Integer status; + + @Schema(description = "语义状态列表", example = "[1, 2]") + private List statuses; + + @Schema(description = "优先级", example = "2") + @InEnum(PmsWorkItemPriorityEnum.class) + private Integer priority; + + @Schema(description = "优先级列表", example = "[2, 3]") + private List priorities; + + @Schema(description = "生命周期状态,1 正常,2 已归档,3 回收站", example = "1") + @InEnum(PmsWorkItemLifecycleStatusEnum.class) + private Integer lifecycleStatus; + + @Schema(description = "看板状态编号", example = "1024") + private Long statusId; + + @Schema(description = "迭代编号", example = "1024") + private Long iterationId; + + @Schema(description = "迭代编号列表", example = "[1024, 2048]") + private List iterationIds; + + @Schema(description = "排除的迭代编号列表", example = "[1024, 2048]") + private List excludedIterationIds; + + @Schema(description = "父工作项编号", example = "1024") + private Long parentId; + + @Schema(description = "标签编号列表", example = "[1024, 2048]") + private List labelIds; + + @Schema(description = "是否只查询未规划工作项", example = "true") + private Boolean unplannedOnly; + + @Schema(description = "是否用于待规划视图;为 true 时只查询一级工作项并按规划顺序返回", example = "true") + private Boolean planningOnly; + + @Schema(description = "是否只查询一级工作项", example = "true") + private Boolean rootOnly; + + @Schema(description = "负责人用户编号", example = "1024") + private Long assigneeUserId; + + @Schema(description = "负责人用户编号列表", example = "[1024, 2048]") + private List assigneeUserIds; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemPlanningSortReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemPlanningSortReqVO.java new file mode 100644 index 000000000..a7fb2783e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemPlanningSortReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +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 = "管理后台 - PMS 待规划和迭代工作项排序 Request VO") +@Data +public class PmsWorkItemPlanningSortReqVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "迭代编号;为空时表示 Backlog", example = "2048") + private Long iterationId; + + @Schema(description = "工作项编号列表,顺序即显示顺序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "工作项编号列表不能为空") + private List<@NotNull(message = "工作项编号不能为空") Long> workItemIds; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemRespVO.java new file mode 100644 index 000000000..92d562804 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemRespVO.java @@ -0,0 +1,125 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label.PmsWorkItemLabelRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 工作项 Response VO") +@Data +@ExcelIgnoreUnannotated +public class PmsWorkItemRespVO { + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long projectId; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @ExcelProperty("类型") + private Integer type; + + @Schema(description = "项目内工作项序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + @ExcelProperty("编号") + private Integer serialNumber; + + @Schema(description = "工作项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "完成登录页") + @ExcelProperty("标题") + private String name; + + @Schema(description = "工作项描述") + @ExcelProperty("描述") + private String description; + + @Schema(description = "优先级", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty("优先级") + private Integer priority; + + @Schema(description = "负责人用户编号", example = "1024") + private Long assigneeUserId; + + @Schema(description = "负责人姓名", example = "芋道源码") + @ExcelProperty("负责人") + private String assigneeUserName; + + @Schema(description = "参与人用户编号列表") + private List memberUserIds; + + @Schema(description = "参与人姓名列表") + private List memberUserNames; + + @Schema(description = "看板状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long statusId; + + @Schema(description = "看板状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "未开始") + @ExcelProperty("状态") + private String statusName; + + @Schema(description = "语义状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "生命周期状态,1 正常,2 已归档,3 回收站", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer lifecycleStatus; + + @Schema(description = "归档时间") + private LocalDateTime archiveTime; + + @Schema(description = "移入回收站时间") + private LocalDateTime recycleTime; + + @Schema(description = "所属迭代编号", example = "1024") + private Long iterationId; + + @Schema(description = "所属迭代名称", example = "第一期") + @ExcelProperty("所属迭代") + private String iterationName; + + @Schema(description = "父工作项编号", example = "1024") + private Long parentId; + + @Schema(description = "关联需求编号", example = "1024") + private Long relatedRequirementId; + + @Schema(description = "关联需求标题", example = "用户登录") + private String relatedRequirementName; + + @Schema(description = "缺陷类型", example = "1") + private Integer defectType; + + @Schema(description = "开始时间") + @ExcelProperty("开始时间") + private LocalDateTime startTime; + + @Schema(description = "截止时间") + @ExcelProperty("截止时间") + private LocalDateTime endTime; + + @Schema(description = "预估工时", example = "8") + @ExcelProperty("预估工时") + private Integer estimatedHours; + + @Schema(description = "完成进度", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + @ExcelProperty("完成进度") + private Integer progress; + + @Schema(description = "附件地址列表") + private List fileUrls; + + @Schema(description = "标签编号列表") + private List labelIds; + + @Schema(description = "标签列表") + private List labels; + + @Schema(description = "看板内显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemSaveReqVO.java new file mode 100644 index 000000000..97ca93a8b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemSaveReqVO.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemDefectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - PMS 工作项新增/修改 Request VO") +@Data +public class PmsWorkItemSaveReqVO { + + @Schema(description = "工作项编号", example = "1024") + private Long id; + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "工作项类型不能为空") + @InEnum(PmsWorkItemTypeEnum.class) + private Integer type; + + @Schema(description = "工作项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "完成登录页") + @NotBlank(message = "工作项标题不能为空") + @Size(max = 100, message = "工作项标题不能超过 100 个字符") + private String name; + + @Schema(description = "工作项描述", example = "实现账号密码登录") + @Size(max = 5000, message = "工作项描述不能超过 5000 个字符") + private String description; + + @Schema(description = "优先级", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "优先级不能为空") + @InEnum(PmsWorkItemPriorityEnum.class) + private Integer priority; + + @Schema(description = "负责人用户编号", example = "1024") + private Long assigneeUserId; + + @Schema(description = "参与人用户编号列表", example = "[1024, 2048]") + private List memberUserIds; + + @Schema(description = "所属迭代编号", example = "1024") + private Long iterationId; + + @Schema(description = "父工作项编号", example = "1024") + private Long parentId; + + @Schema(description = "关联需求编号", example = "1024") + private Long relatedRequirementId; + + @Schema(description = "缺陷类型", example = "1") + @InEnum(PmsWorkItemDefectTypeEnum.class) + private Integer defectType; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "截止时间") + private LocalDateTime endTime; + + @Schema(description = "预估工时,单位:小时", example = "8") + @Min(value = 0, message = "预估工时不能小于 0") + private Integer estimatedHours; + + @Schema(description = "完成进度", example = "50") + @NotNull(message = "完成进度不能为空") + @Min(value = 0, message = "完成进度不能小于 0") + @Max(value = 100, message = "完成进度不能大于 100") + private Integer progress; + + @Schema(description = "附件地址列表") + private List fileUrls; + + @Schema(description = "标签编号列表", example = "[1024, 2048]") + private List labelIds; + + @Schema(description = "创建时同时新增的子工作项标题列表") + private List<@NotBlank(message = "子工作项标题不能为空") + @Size(max = 100, message = "子工作项标题不能超过 100 个字符") String> childWorkItemNames; + + @Schema(description = "创建时登记的实际投入工时,单位:小时", example = "2") + @Min(value = 1, message = "实际投入工时必须大于 0") + private Integer actualHours; + + @Schema(description = "创建时登记的剩余工时,单位:小时", example = "6") + @Min(value = 0, message = "剩余工时不能小于 0") + private Integer remainingHours; + + @AssertTrue(message = "工作项结束时间必须晚于开始时间") + public boolean isTimeValid() { + return startTime == null || endTime == null || startTime.isBefore(endTime); + } + + @AssertTrue(message = "填写剩余工时时必须同时填写实际投入工时") + public boolean isWorkLogValid() { + return remainingHours == null || actualHours != null; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemSortReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemSortReqVO.java new file mode 100644 index 000000000..550ea3fc5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemSortReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem; + +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 = "管理后台 - PMS 工作项看板排序 Request VO") +@Data +public class PmsWorkItemSortReqVO { + + @Schema(description = "看板状态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "看板状态不能为空") + private Long statusId; + + @Schema(description = "工作项编号列表,顺序即看板显示顺序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "工作项编号列表不能为空") + private List<@NotNull(message = "工作项编号不能为空") Long> workItemIds; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsProjectWorkLogReportReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsProjectWorkLogReportReqVO.java new file mode 100644 index 000000000..45ab7e8ff --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsProjectWorkLogReportReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - PMS 项目工时报表 Request VO") +@Data +public class PmsProjectWorkLogReportReqVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "工时登记时间范围", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "工时登记时间范围不能为空") + @Size(min = 2, max = 2, message = "工时登记时间范围必须包含开始时间和结束时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "迭代名称", example = "第一迭代") + private String iterationName; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsProjectWorkLogReportRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsProjectWorkLogReportRespVO.java new file mode 100644 index 000000000..57132e6b8 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsProjectWorkLogReportRespVO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - PMS 项目工时报表 Response VO") +@Data +public class PmsProjectWorkLogReportRespVO { + + @Schema(description = "报表日期列表") + private List dates; + + @Schema(description = "总工时") + private Integer totalHours; + + @Schema(description = "迭代分组") + private List groups; + + @Data + public static class Group { + + @Schema(description = "迭代编号,未规划事项为空") + private Long iterationId; + + @Schema(description = "迭代名称") + private String iterationName; + + @Schema(description = "分组总工时") + private Integer totalHours; + + @Schema(description = "工作项列表") + private List items; + + } + + @Schema(description = "管理后台 - PMS 项目工时报表工作项明细") + @Data + public static class Item { + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long workItemId; + + @Schema(description = "项目内工作项序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + private Integer serialNumber; + + @Schema(description = "工作项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "完成登录接口") + private String name; + + @Schema(description = "工作项类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer type; + + @Schema(description = "工作项总工时", requiredMode = Schema.RequiredMode.REQUIRED, example = "8") + private Integer totalHours; + + @Schema(description = "每日工时,键为日期,值为小时数", requiredMode = Schema.RequiredMode.REQUIRED, + example = "{\"2026-08-22\": 4}") + private Map dailyHours; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogRespVO.java new file mode 100644 index 000000000..085428649 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - PMS 工作项工时记录 Response VO") +@Data +public class PmsWorkItemWorkLogRespVO { + + @Schema(description = "工时记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long workItemId; + + @Schema(description = "工作项标题", example = "完成登录接口") + private String workItemName; + + @Schema(description = "实际投入工时,单位:小时", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") + private Integer actualHours; + + @Schema(description = "本次登记后的剩余工时,单位:小时", requiredMode = Schema.RequiredMode.REQUIRED, + example = "8") + private Integer remainingHours; + + @Schema(description = "工时说明", example = "完成登录接口联调") + private String description; + + @Schema(description = "登记人用户编号", example = "1") + private Long creatorUserId; + + @Schema(description = "登记人姓名", example = "芋道源码") + private String creatorUserName; + + @Schema(description = "登记时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogSaveReqVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogSaveReqVO.java new file mode 100644 index 000000000..07e60d6ae --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogSaveReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - PMS 工作项工时记录新增/修改 Request VO") +@Data +public class PmsWorkItemWorkLogSaveReqVO { + + @Schema(description = "工时记录编号", example = "1024") + private Long id; + + @Schema(description = "工作项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "工作项编号不能为空") + private Long workItemId; + + @Schema(description = "实际投入工时,单位:小时", requiredMode = Schema.RequiredMode.REQUIRED, example = "4") + @NotNull(message = "实际投入工时不能为空") + @Min(value = 1, message = "实际投入工时必须大于 0") + private Integer actualHours; + + @Schema(description = "本次登记后的剩余工时,单位:小时", example = "8") + @Min(value = 0, message = "剩余工时不能小于 0") + private Integer remainingHours; + + @Schema(description = "工时说明", example = "完成登录接口联调") + @Size(max = 500, message = "工时说明不能超过 500 个字符") + private String description; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogSummaryRespVO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogSummaryRespVO.java new file mode 100644 index 000000000..91976aada --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/worklog/PmsWorkItemWorkLogSummaryRespVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - PMS 工作项工时汇总 Response VO") +@Data +public class PmsWorkItemWorkLogSummaryRespVO { + + @Schema(description = "预估工时,单位:小时", example = "16") + private Integer estimatedHours; + + @Schema(description = "已登记工时,单位:小时", requiredMode = Schema.RequiredMode.REQUIRED, example = "8") + private Integer actualHours; + + @Schema(description = "剩余工时,单位:小时", example = "8") + private Integer remainingHours; + + @Schema(description = "工时记录列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List records; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/package-info.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/package-info.java new file mode 100644 index 000000000..117f27385 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/controller/package-info.java @@ -0,0 +1,4 @@ +/** + * PMS 模块的 Controller,提供管理后台和用户端 API + */ +package cn.iocoder.yudao.module.pms.controller; diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeContentPermissionDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeContentPermissionDO.java new file mode 100644 index 000000000..607c913e2 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeContentPermissionDO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeContentLevelEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 知识内容协作权限 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_content_permission") +@KeySequence("pms_knowledge_content_permission_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeContentPermissionDO extends BaseDO { + + /** + * 内容协作权限编号 + */ + @TableId + private Long id; + /** + * 知识库编号 + * + * 关联 {@link PmsKnowledgeLibraryDO#getId()} + */ + private Long libraryId; + /** + * 是否对知识库可访问人公开 + */ + private Boolean openStatus; + /** + * 公开协作等级 + * + * 枚举 {@link PmsKnowledgeContentLevelEnum} + */ + private Integer openLevel; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeContentPermissionMemberDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeContentPermissionMemberDO.java new file mode 100644 index 000000000..e1377cbe1 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeContentPermissionMemberDO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeContentLevelEnum; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 知识内容协作者 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_content_permission_member") +@KeySequence("pms_knowledge_content_permission_member_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeContentPermissionMemberDO extends BaseDO { + + /** + * 协作者编号 + */ + @TableId + private Long id; + /** + * 内容协作权限编号 + * + * 关联 {@link PmsKnowledgeContentPermissionDO#getId()} + */ + private Long permissionId; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 部门编号 + * + * 关联 {@link DeptRespDTO#getId()} + */ + private Long deptId; + /** + * 协作等级 + * + * 枚举 {@link PmsKnowledgeContentLevelEnum} + */ + private Integer level; +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeDocumentDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeDocumentDO.java new file mode 100644 index 000000000..adcedc216 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeDocumentDO.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * PMS 知识库文档 DO + * + * @author 芋道源码 + */ +@TableName(value = "pms_knowledge_document", autoResultMap = true) +@KeySequence("pms_knowledge_document_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeDocumentDO extends BaseDO { + + /** + * 根文件夹编号 + */ + public static final Long FOLDER_ID_ROOT = 0L; + /** + * 根文档编号 + */ + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 文档编号 + */ + @TableId + private Long id; + /** + * 知识库编号 + * + * 关联 {@link PmsKnowledgeLibraryDO#getId()} + */ + private Long libraryId; + /** + * 内容协作权限编号 + * + * 关联 {@link PmsKnowledgeContentPermissionDO#getId()} + */ + private Long permissionId; + /** + * 文件夹编号,0 表示不在文件夹中 + * + * 关联 {@link PmsKnowledgeFolderDO#getId()} + */ + private Long folderId; + /** + * 父文档编号,0 表示根文档 + * + * 关联 {@link #getId()} + */ + private Long parentId; + /** + * 文档标题 + */ + private String title; + /** + * 文档内容或文件地址 + */ + private String content; + /** + * 文档类型 + * + * 枚举 {@link PmsKnowledgeDocumentTypeEnum} + */ + private Integer type; + /** + * 文件类型 + */ + private String fileType; + /** + * 文件大小,单位:字节 + */ + private Long fileSize; + /** + * 文档状态 + * + * 枚举 {@link PmsKnowledgeDocumentStatusEnum} + */ + private Integer status; + /** + * 标签编号列表 + * + * 关联 {@link PmsKnowledgeDocumentLabelDO#getId()} + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List labelIds; + /** + * 删除人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long deleteUserId; + /** + * 删除时间 + */ + private LocalDateTime deleteTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeDocumentLabelDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeDocumentLabelDO.java new file mode 100644 index 000000000..d7cf32258 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeDocumentLabelDO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 知识库文档标签 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_document_label") +@KeySequence("pms_knowledge_document_label_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeDocumentLabelDO extends BaseDO { + + /** + * 文档标签编号 + */ + @TableId + private Long id; + /** + * 标签名称 + */ + private String name; + /** + * 标签颜色 + */ + private String color; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeFolderDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeFolderDO.java new file mode 100644 index 000000000..338424a13 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/content/PmsKnowledgeFolderDO.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * PMS 知识库文件夹 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_folder") +@KeySequence("pms_knowledge_folder_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeFolderDO extends BaseDO { + + /** + * 根文件夹编号 + */ + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 文件夹编号 + */ + @TableId + private Long id; + /** + * 知识库编号 + * + * 关联 {@link PmsKnowledgeLibraryDO#getId()} + */ + private Long libraryId; + /** + * 内容协作权限编号 + * + * 关联 {@link PmsKnowledgeContentPermissionDO#getId()} + */ + private Long permissionId; + /** + * 父文件夹编号,0 表示根目录 + * + * 关联 {@link #getId()} + */ + private Long parentId; + /** + * 文件夹标题 + */ + private String title; + /** + * 文件夹状态 + * + * 枚举 {@link PmsKnowledgeDocumentStatusEnum} + */ + private Integer status; + /** + * 删除人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long deleteUserId; + /** + * 删除时间 + */ + private LocalDateTime deleteTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentCommentDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentCommentDO.java new file mode 100644 index 000000000..b234d94d6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentCommentDO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 知识库文档评论 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_document_comment") +@KeySequence("pms_knowledge_document_comment_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeDocumentCommentDO extends BaseDO { + + /** + * 主评论编号 + */ + public static final Long MAIN_ID_ROOT = 0L; + + /** + * 评论编号 + */ + @TableId + private Long id; + /** + * 文档编号 + * + * 关联 {@link PmsKnowledgeDocumentDO#getId()} + */ + private Long documentId; + /** + * 评论人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 主评论编号,0 表示主评论 + * + * 关联 {@link #getId()} + */ + private Long mainId; + /** + * 回复对象用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long replyUserId; + /** + * 评论内容 + */ + private String content; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentLikeDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentLikeDO.java new file mode 100644 index 000000000..1a9fefd5a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentLikeDO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 知识文档点赞 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_document_like") +@KeySequence("pms_knowledge_document_like_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeDocumentLikeDO extends BaseDO { + + /** + * 点赞编号 + */ + @TableId + private Long id; + /** + * 文档编号 + * + * 关联 {@link PmsKnowledgeDocumentDO#getId()} + */ + private Long documentId; + /** + * 点赞人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentShareDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentShareDO.java new file mode 100644 index 000000000..a145d892b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeDocumentShareDO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * PMS 知识库文档分享 DO + * + * @author 芋道源码 + */ +@TableName(value = "pms_knowledge_document_share", autoResultMap = true) +@KeySequence("pms_knowledge_document_share_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeDocumentShareDO extends BaseDO { + + /** + * 分享编号 + */ + @TableId + private Long id; + /** + * 文档编号 + * + * 关联 {@link PmsKnowledgeDocumentDO#getId()} + */ + private Long documentId; + /** + * 内部分享成员用户编号列表 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List shareUserIds; + /** + * 外部查看令牌 + */ + private String token; + /** + * 分享状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 关闭人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long closeUserId; + /** + * 关闭时间 + */ + private LocalDateTime closeTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeFavoriteDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeFavoriteDO.java new file mode 100644 index 000000000..4ecb1498f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeFavoriteDO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 知识收藏(关注)DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_favorite") +@KeySequence("pms_knowledge_favorite_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeFavoriteDO extends BaseDO { + + /** + * 收藏编号 + */ + @TableId + private Long id; + /** + * 知识库编号 + * + * 关联 {@link PmsKnowledgeLibraryDO#getId()} + */ + private Long libraryId; + /** + * 收藏对象类型 + * + * 枚举 {@link PmsKnowledgeObjectTypeEnum} + */ + private Integer type; + /** + * 收藏对象编号 + * + * 根据 {@link #getType()} 关联知识库、文件夹或文档编号 + */ + private Long entityId; + /** + * 收藏人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeViewRecordDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeViewRecordDO.java new file mode 100644 index 000000000..7d0f2447f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/interaction/PmsKnowledgeViewRecordDO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 知识最近浏览记录 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_view_record") +@KeySequence("pms_knowledge_view_record_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeViewRecordDO extends BaseDO { + + /** + * 浏览记录编号 + */ + @TableId + private Long id; + /** + * 知识库编号 + * + * 关联 {@link PmsKnowledgeLibraryDO#getId()} + */ + private Long libraryId; + /** + * 浏览对象类型 + * + * 枚举 {@link PmsKnowledgeObjectTypeEnum} + */ + private Integer type; + /** + * 浏览对象编号 + * + * 根据 {@link #getType()} 关联文件夹或文档编号 + */ + private Long entityId; + /** + * 浏览人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeGroupDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeGroupDO.java new file mode 100644 index 000000000..4b985324f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeGroupDO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.library; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeGroupTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 个人知识库分组 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_group") +@KeySequence("pms_knowledge_group_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeGroupDO extends BaseDO { + + /** + * 知识库分组编号 + */ + @TableId + private Long id; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 分组名称 + */ + private String name; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 分组类型 + * + * 枚举 {@link PmsKnowledgeGroupTypeEnum} + */ + private Integer type; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeGroupRelationDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeGroupRelationDO.java new file mode 100644 index 000000000..e3869ae06 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeGroupRelationDO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.library; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 个人知识库分组关系 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_group_relation") +@KeySequence("pms_knowledge_group_relation_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeGroupRelationDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 知识库分组编号 + * + * 关联 {@link PmsKnowledgeGroupDO#getId()} + */ + private Long groupId; + /** + * 知识库编号 + * + * 关联 {@link PmsKnowledgeLibraryDO#getId()} + */ + private Long libraryId; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryDO.java new file mode 100644 index 000000000..97bd2776b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryDO.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.library; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * PMS 知识库 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_library") +@KeySequence("pms_knowledge_library_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeLibraryDO extends BaseDO { + + /** + * 知识库编号 + */ + @TableId + private Long id; + /** + * 知识库名称 + */ + private String name; + /** + * 知识库简介 + */ + private String description; + /** + * 是否公开 + */ + private Boolean openStatus; + /** + * 知识库封面 + */ + private String coverUrl; + /** + * 知识库状态 + * + * 枚举 {@link PmsKnowledgeDocumentStatusEnum} + */ + private Integer status; + /** + * 删除人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long deleteUserId; + /** + * 删除时间 + */ + private LocalDateTime deleteTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryMemberDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryMemberDO.java new file mode 100644 index 000000000..185e842d4 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryMemberDO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.library; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 知识库成员 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_library_member") +@KeySequence("pms_knowledge_library_member_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeLibraryMemberDO extends BaseDO { + + /** + * 成员编号 + */ + @TableId + private Long id; + /** + * 知识库编号 + * + * 关联 {@link PmsKnowledgeLibraryDO#getId()} + */ + private Long libraryId; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 部门编号 + * + * 关联 {@link DeptRespDTO#getId()} + */ + private Long deptId; + /** + * 成员等级 + * + * 枚举 {@link PmsKnowledgeLibraryMemberLevelEnum} + */ + private Integer level; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryTemplateDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryTemplateDO.java new file mode 100644 index 000000000..53ba3b652 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/library/PmsKnowledgeLibraryTemplateDO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.library; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +/** + * PMS 知识库模板 DO + * + *

模板只保存新建知识库时使用的默认文档,知识库创建后会生成独立的文档内容

+ * + * @author 芋道源码 + */ +@TableName(value = "pms_knowledge_library_template", autoResultMap = true) +@KeySequence("pms_knowledge_library_template_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeLibraryTemplateDO extends BaseDO { + + /** + * 模板编号 + */ + @TableId + private Long id; + /** + * 模板名称 + */ + private String name; + /** + * 模板简介 + */ + private String description; + /** + * 模板封面地址 + */ + private String coverUrl; + /** + * 模板状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 模板文档列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List documents; + + /** + * 模板文档 + */ + @Data + public static class Document { + + /** + * 文档标题 + */ + private String title; + /** + * 文档内容 + */ + private String content; + + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/recycle/PmsKnowledgeRecycleRecordDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/recycle/PmsKnowledgeRecycleRecordDO.java new file mode 100644 index 000000000..6241e895a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/kb/recycle/PmsKnowledgeRecycleRecordDO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.kb.recycle; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * PMS 知识库回收站记录 DO + * + * @author 芋道源码 + */ +@TableName("pms_knowledge_recycle_record") +@KeySequence("pms_knowledge_recycle_record_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsKnowledgeRecycleRecordDO extends BaseDO { + + /** + * 回收站记录编号 + */ + @TableId + private Long id; + /** + * 知识库编号 + * + * 关联 {@link PmsKnowledgeLibraryDO#getId()} + */ + private Long libraryId; + /** + * 回收对象类型 + * + * 枚举 {@link PmsKnowledgeObjectTypeEnum} + */ + private Integer type; + /** + * 回收对象编号 + * + * 根据 {@link #getType()} 关联知识库、文件夹或文档编号 + */ + private Long entityId; + /** + * 回收对象名称 + */ + private String name; + /** + * 删除人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long deleteUserId; + /** + * 删除时间 + */ + private LocalDateTime deleteTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/iteration/PmsIterationDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/iteration/PmsIterationDO.java new file mode 100644 index 000000000..e03fa05f1 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/iteration/PmsIterationDO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.enums.pm.iteration.PmsIterationStatusEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * PMS 项目迭代 DO + * + * @author 芋道源码 + */ +@TableName("pms_iteration") +@KeySequence("pms_iteration_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsIterationDO extends BaseDO { + + /** + * 迭代编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 迭代名称 + */ + private String name; + /** + * 负责人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long ownerUserId; + /** + * 迭代状态 + * + * 枚举 {@link PmsIterationStatusEnum} + */ + private Integer status; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 结束时间 + */ + private LocalDateTime endTime; + /** + * 完成时间 + */ + private LocalDateTime finishTime; + /** + * 迭代目标 + */ + private String target; + /** + * 迭代描述 + */ + private String description; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectAnnouncementDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectAnnouncementDO.java new file mode 100644 index 000000000..fca831da4 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectAnnouncementDO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +/** + * PMS 项目公告 DO + * + * @author 芋道源码 + */ +@TableName(value = "pms_project_announcement", autoResultMap = true) +@KeySequence("pms_project_announcement_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsProjectAnnouncementDO extends BaseDO { + + /** + * 公告编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 公告内容 + */ + private String content; + /** + * 附件地址列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List fileUrls; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectDO.java new file mode 100644 index 000000000..60ef75392 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectDO.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectLevelEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * PMS 项目 DO + * + * @author 芋道源码 + */ +@TableName("pms_project") +@KeySequence("pms_project_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsProjectDO extends BaseDO { + + /** + * 项目编号 + */ + @TableId + private Long id; + /** + * 项目名称 + */ + private String name; + /** + * 项目状态 + * + * 枚举 {@link PmsProjectStatusEnum} + */ + private Integer status; + /** + * 项目类型 + * + * 枚举 {@link PmsProjectTypeEnum} + */ + private Integer type; + /** + * 项目优先级 + * + * 枚举 {@link PmsProjectLevelEnum} + */ + private Integer level; + /** + * 项目描述 + */ + private String description; + /** + * 是否公开 + */ + private Boolean openStatus; + /** + * 项目图标 + */ + private String icon; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 截止时间 + */ + private LocalDateTime endTime; + /** + * 归档时间 + */ + private LocalDateTime archiveTime; + /** + * 移入回收站时间 + */ + private LocalDateTime recycleTime; + /** + * 最近访问时间 + */ + private LocalDateTime accessTime; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectFavoriteDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectFavoriteDO.java new file mode 100644 index 000000000..2b4169dcf --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectFavoriteDO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 项目收藏 DO + * + * @author 芋道源码 + */ +@TableName("pms_project_favorite") +@KeySequence("pms_project_favorite_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsProjectFavoriteDO extends BaseDO { + + /** + * 收藏编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 收藏人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectGroupDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectGroupDO.java new file mode 100644 index 000000000..4c41d4cfb --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectGroupDO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectGroupTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 项目分组 DO + * + * 项目分组是后台用户个人维护的项目视图,不影响其他成员看到的项目分组 + * + * @author 芋道源码 + */ +@TableName("pms_project_group") +@KeySequence("pms_project_group_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsProjectGroupDO extends BaseDO { + + /** + * 分组编号 + */ + @TableId + private Long id; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 分组名称 + */ + private String name; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 分组类型 + * + * 枚举 {@link PmsProjectGroupTypeEnum} + */ + private Integer type; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectGroupRelationDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectGroupRelationDO.java new file mode 100644 index 000000000..216cd1cd2 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectGroupRelationDO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 项目分组关系 DO + * + * @author 芋道源码 + */ +@TableName("pms_project_group_relation") +@KeySequence("pms_project_group_relation_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsProjectGroupRelationDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 项目分组编号 + * + * 关联 {@link PmsProjectGroupDO#getId()} + */ + private Long groupId; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectMemberDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectMemberDO.java new file mode 100644 index 000000000..604ded2de --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectMemberDO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectMemberLevelEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 项目成员 DO + * + * @author 芋道源码 + */ +@TableName("pms_project_member") +@KeySequence("pms_project_member_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsProjectMemberDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 成员权限级别 + * + * 枚举 {@link PmsProjectMemberLevelEnum} + */ + private Integer level; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectTemplateDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectTemplateDO.java new file mode 100644 index 000000000..a1070aa81 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/project/PmsProjectTemplateDO.java @@ -0,0 +1,150 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.project; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +/** + * PMS 项目模板 DO + * + *

模板只保存新建项目时使用的默认协作配置,项目创建后会生成自己的运行配置

+ * + * @author 芋道源码 + */ +@TableName(value = "pms_project_template", autoResultMap = true) +@KeySequence("pms_project_template_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsProjectTemplateDO extends BaseDO { + + /** + * 模板编号 + */ + @TableId + private Long id; + /** + * 模板名称 + */ + private String name; + /** + * 模板描述 + */ + private String description; + /** + * 项目类型 + * + * 枚举 {@link PmsProjectTypeEnum} + */ + private Integer projectType; + /** + * 模板状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 启用的工作项类型列表 + * + * 枚举 {@link PmsWorkItemTypeEnum} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List itemTypes; + /** + * 工作项状态模板列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List statuses; + /** + * 看板列模板列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List boards; + + /** + * 工作项状态模板 + */ + @Data + public static class StatusTemplate { + + /** + * 模板内稳定编码 + */ + private String code; + /** + * 状态名称 + */ + private String name; + /** + * 工作项类型 + * + * 枚举 {@link PmsWorkItemTypeEnum} + */ + private Integer workItemType; + /** + * 语义状态 + * + * 枚举 {@link PmsWorkItemStatusTypeEnum} + */ + private Integer statusType; + /** + * 是否为初始状态 + */ + private Boolean defaultStatus; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 所属看板列编码 + */ + private String boardCode; + } + + /** + * 看板列模板 + */ + @Data + public static class BoardTemplate { + + /** + * 模板内稳定编码 + */ + private String code; + /** + * 看板列名称 + */ + private String name; + /** + * 工作项类型 + * + * 枚举 {@link PmsWorkItemTypeEnum} + */ + private Integer workItemType; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 看板列关联的状态编码列表 + */ + private List statusCodes; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemActivityDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemActivityDO.java new file mode 100644 index 000000000..a74c5b81a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemActivityDO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 工作项动态 DO + * + * @author 芋道源码 + */ +@TableName("pms_work_item_activity") +@KeySequence("pms_work_item_activity_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemActivityDO extends BaseDO { + + /** + * 动态编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 工作项编号 + * + * 关联 {@link PmsWorkItemDO#getId()} + */ + private Long workItemId; + /** + * 操作人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long operatorUserId; + /** + * 动态内容 + */ + private String content; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemBoardDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemBoardDO.java new file mode 100644 index 000000000..d8f84a76e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemBoardDO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 工作项看板列 DO + * + * @author 芋道源码 + */ +@TableName("pms_work_item_board") +@KeySequence("pms_work_item_board_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemBoardDO extends BaseDO { + + /** + * 看板列编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 工作项类型 + * + * 枚举 {@link PmsWorkItemTypeEnum} + */ + private Integer workItemType; + /** + * 看板列名称 + */ + private String name; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemCommentDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemCommentDO.java new file mode 100644 index 000000000..76f96b3fa --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemCommentDO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 工作项评论 DO + * + * @author 芋道源码 + */ +@TableName("pms_work_item_comment") +@KeySequence("pms_work_item_comment_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemCommentDO extends BaseDO { + + /** + * 主评论使用的根编号 + */ + public static final Long MAIN_ID_ROOT = 0L; + + /** + * 评论编号 + */ + @TableId + private Long id; + /** + * 工作项编号 + * + * 关联 {@link PmsWorkItemDO#getId()} + */ + private Long workItemId; + /** + * 评论人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 主评论编号,{@link #MAIN_ID_ROOT} 表示主评论 + * + * 关联 {@link #getId()} + */ + private Long mainId; + /** + * 回复对象用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long replyUserId; + /** + * 评论内容 + */ + private String content; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemDO.java new file mode 100644 index 000000000..5dd95d0a7 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemDO.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemDefectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemLifecycleStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * PMS 工作项 DO + * + * @author 芋道源码 + */ +@TableName(value = "pms_work_item", autoResultMap = true) +@KeySequence("pms_work_item_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemDO extends BaseDO { + + /** + * 工作项编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 工作项类型 + * + * 枚举 {@link PmsWorkItemTypeEnum} + */ + private Integer type; + /** + * 项目内工作项序号 + */ + private Integer serialNumber; + /** + * 工作项标题 + */ + private String name; + /** + * 工作项描述 + */ + private String description; + /** + * 优先级 + * + * 枚举 {@link PmsWorkItemPriorityEnum} + */ + private Integer priority; + /** + * 负责人用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long assigneeUserId; + /** + * 看板状态编号 + * + * 关联 {@link PmsWorkItemStatusDO#getId()} + */ + private Long statusId; + /** + * 语义状态 + * + * 枚举 {@link PmsWorkItemStatusTypeEnum} + */ + private Integer status; + /** + * 生命周期状态 + * + * 枚举 {@link PmsWorkItemLifecycleStatusEnum} + */ + private Integer lifecycleStatus; + /** + * 归档时间 + */ + private LocalDateTime archiveTime; + /** + * 移入回收站时间 + */ + private LocalDateTime recycleTime; + /** + * 所属迭代编号 + * + * 关联 {@link PmsIterationDO#getId()} + */ + private Long iterationId; + /** + * 父工作项编号 + * + * 关联 {@link #getId()} + */ + private Long parentId; + /** + * 关联需求编号 + * + * 关联 {@link #getId()} + */ + private Long relatedRequirementId; + /** + * 缺陷类型 + * + * 枚举 {@link PmsWorkItemDefectTypeEnum} + */ + private Integer defectType; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 截止时间 + */ + private LocalDateTime endTime; + /** + * 预估工时,单位:小时 + */ + private Integer estimatedHours; + /** + * 完成进度,取值范围 0-100 + */ + private Integer progress; + /** + * 附件地址列表 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List fileUrls; + /** + * 标签编号列表 + * + * 关联 {@link PmsWorkItemLabelDO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List labelIds; + /** + * 看板内显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemLabelDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemLabelDO.java new file mode 100644 index 000000000..5ee1532fb --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemLabelDO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 工作项标签 DO + * + * @author 芋道源码 + */ +@TableName("pms_work_item_label") +@KeySequence("pms_work_item_label_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemLabelDO extends BaseDO { + + /** + * 标签编号 + */ + @TableId + private Long id; + /** + * 标签名称 + */ + private String name; + /** + * 标签颜色 + */ + private String color; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemMemberDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemMemberDO.java new file mode 100644 index 000000000..5b338ea3b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemMemberDO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 工作项参与人 DO + * + * @author 芋道源码 + */ +@TableName("pms_work_item_member") +@KeySequence("pms_work_item_member_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemMemberDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 工作项编号 + * + * 关联 {@link PmsWorkItemDO#getId()} + */ + private Long workItemId; + /** + * 后台用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemStatusDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemStatusDO.java new file mode 100644 index 000000000..6fb9ea74b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemStatusDO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 工作项看板状态 DO + * + * @author 芋道源码 + */ +@TableName("pms_work_item_status") +@KeySequence("pms_work_item_status_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemStatusDO extends BaseDO { + + /** + * 状态编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 工作项类型 + * + * 枚举 {@link PmsWorkItemTypeEnum} + */ + private Integer workItemType; + /** + * 状态名称 + */ + private String name; + /** + * 语义状态 + * + * 枚举 {@link PmsWorkItemStatusTypeEnum} + */ + private Integer statusType; + /** + * 状态描述 + */ + private String description; + /** + * 所属看板列名称,为空时不在看板展示 + */ + private String boardName; + /** + * 系统状态编码,仅默认状态使用,用于保证并发初始化幂等 + */ + private String systemCode; + /** + * 是否初始状态 + */ + private Boolean defaultStatus; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemUserSortDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemUserSortDO.java new file mode 100644 index 000000000..1c3ad44aa --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemUserSortDO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 工作项个人排序 DO + * + * @author 芋道源码 + */ +@TableName("pms_work_item_user_sort") +@KeySequence("pms_work_item_user_sort_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemUserSortDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 工作项编号 + * + * 关联 {@link PmsWorkItemDO#getId()} + */ + private Long workItemId; + /** + * 用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + /** + * 显示顺序 + */ + private Integer sort; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemWorkLogDO.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemWorkLogDO.java new file mode 100644 index 000000000..6ae5e53e3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/dataobject/pm/workitem/PmsWorkItemWorkLogDO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * PMS 工作项工时记录 DO + * + * @author 芋道源码 + */ +@TableName("pms_work_item_work_log") +@KeySequence("pms_work_item_work_log_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PmsWorkItemWorkLogDO extends BaseDO { + + /** + * 工时记录编号 + */ + @TableId + private Long id; + /** + * 项目编号 + * + * 关联 {@link PmsProjectDO#getId()} + */ + private Long projectId; + /** + * 工作项编号 + * + * 关联 {@link PmsWorkItemDO#getId()} + */ + private Long workItemId; + /** + * 实际投入工时,单位:小时 + */ + private Integer actualHours; + /** + * 本次登记后的剩余工时,单位:小时 + */ + private Integer remainingHours; + /** + * 工时说明 + */ + private String description; + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeContentPermissionMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeContentPermissionMapper.java new file mode 100644 index 000000000..50b6565f4 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeContentPermissionMapper.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsKnowledgeContentPermissionMapper extends BaseMapperX { + + default List selectListByLibraryIds(Collection libraryIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeContentPermissionDO::getLibraryId, libraryIds)); + } + + default List selectListByLibraryId(Long libraryId) { + return selectList(PmsKnowledgeContentPermissionDO::getLibraryId, libraryId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeContentPermissionMemberMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeContentPermissionMemberMapper.java new file mode 100644 index 000000000..87216c364 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeContentPermissionMemberMapper.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionMemberDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsKnowledgeContentPermissionMemberMapper extends BaseMapperX { + + default List selectListByPermissionId(Long permissionId) { + return selectList(PmsKnowledgeContentPermissionMemberDO::getPermissionId, permissionId); + } + + default List selectListByPermissionIds(Collection permissionIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeContentPermissionMemberDO::getPermissionId, permissionIds)); + } + + default PmsKnowledgeContentPermissionMemberDO selectByPermissionIdAndUserId(Long permissionId, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeContentPermissionMemberDO::getPermissionId, permissionId) + .eq(PmsKnowledgeContentPermissionMemberDO::getUserId, userId)); + } + + default void deleteByPermissionId(Long permissionId) { + delete(PmsKnowledgeContentPermissionMemberDO::getPermissionId, permissionId); + } + + default void deleteByPermissionIds(Collection permissionIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeContentPermissionMemberDO::getPermissionId, permissionIds)); + } + + default void deleteByPermissionIdsAndUserIds(Collection permissionIds, Collection userIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeContentPermissionMemberDO::getPermissionId, permissionIds) + .in(PmsKnowledgeContentPermissionMemberDO::getUserId, userIds)); + } + + default void deleteByPermissionIdsAndDeptIds(Collection permissionIds, Collection deptIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeContentPermissionMemberDO::getPermissionId, permissionIds) + .in(PmsKnowledgeContentPermissionMemberDO::getDeptId, deptIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeDocumentLabelMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeDocumentLabelMapper.java new file mode 100644 index 000000000..2787568d8 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeDocumentLabelMapper.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentLabelDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PmsKnowledgeDocumentLabelMapper extends BaseMapperX { + + default List selectList() { + return selectList(new LambdaQueryWrapperX() + .orderByAsc(PmsKnowledgeDocumentLabelDO::getCreateTime) + .orderByAsc(PmsKnowledgeDocumentLabelDO::getId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeDocumentMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeDocumentMapper.java new file mode 100644 index 000000000..dd7a1a9e6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeDocumentMapper.java @@ -0,0 +1,144 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentSearchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Mapper +public interface PmsKnowledgeDocumentMapper extends BaseMapperX { + + default Set selectExistingPermissionIdSet(Collection permissionIds) { + return convertSet(selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeDocumentDO::getPermissionId, permissionIds) + .select(PmsKnowledgeDocumentDO::getPermissionId)), PmsKnowledgeDocumentDO::getPermissionId); + } + + /** + * 统计各知识库下不同文档类型的数量 + * + * @param libraryIds 知识库编号集合 + * @param status 文档状态 + * @return 知识库编号 ->(文档类型 -> 文档数量) + */ + default Map> selectTypeCountMapByLibraryIdsAndStatus( + Collection libraryIds, Integer status) { + if (CollUtil.isEmpty(libraryIds)) { + return Collections.emptyMap(); + } + List> rows = selectMaps(new QueryWrapperX() + .select("library_id AS libraryId", "type", "COUNT(*) AS count") + .in("library_id", libraryIds) + .eq("status", status) + .groupBy("library_id", "type")); + Map> result = new LinkedHashMap<>(); + for (Map row : rows) { + Long libraryId = ((Number) row.get("libraryId")).longValue(); + Integer type = ((Number) row.get("type")).intValue(); + result.computeIfAbsent(libraryId, key -> new LinkedHashMap<>()) + .put(type, ((Number) row.get("count")).longValue()); + } + return result; + } + + default List selectListByLibraryIdAndStatus(Long libraryId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeDocumentDO::getLibraryId, libraryId) + .eq(PmsKnowledgeDocumentDO::getStatus, status) + .orderByAsc(PmsKnowledgeDocumentDO::getCreateTime) + .orderByAsc(PmsKnowledgeDocumentDO::getId)); + } + + default List selectListByLibraryId(Long libraryId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeDocumentDO::getLibraryId, libraryId) + .orderByAsc(PmsKnowledgeDocumentDO::getId)); + } + + default List selectListByParentIds(Collection parentIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeDocumentDO::getParentId, parentIds)); + } + + default List selectListByFolderIds(Collection folderIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeDocumentDO::getFolderId, folderIds)); + } + + default int updateToRestoreByIds(Collection ids, Integer status) { + return update(new LambdaUpdateWrapper() + .set(PmsKnowledgeDocumentDO::getStatus, status) + .set(PmsKnowledgeDocumentDO::getDeleteUserId, null) + .set(PmsKnowledgeDocumentDO::getDeleteTime, null) + .in(PmsKnowledgeDocumentDO::getId, ids)); + } + + /** + * 按标签和状态分页查询文档,并限制在用户可读的知识库和权限范围内 + * + * @param pageReqVO 标签分页查询条件 + * @param readableLibraryIds 用户可读的知识库编号集合 + * @param readablePermissionIds 用户可读的内容权限编号集合 + * @param statuses 文档状态集合 + * @return 文档分页结果 + */ + default PageResult selectPageByLabelIdAndStatuses( + PmsKnowledgeDocumentLabelPageReqVO pageReqVO, Collection readableLibraryIds, + Collection readablePermissionIds, Collection statuses) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + queryWrapper.in(PmsKnowledgeDocumentDO::getLibraryId, readableLibraryIds) + .in(PmsKnowledgeDocumentDO::getPermissionId, readablePermissionIds) + .in(PmsKnowledgeDocumentDO::getStatus, statuses) + .orderByDesc(PmsKnowledgeDocumentDO::getUpdateTime) + .orderByDesc(PmsKnowledgeDocumentDO::getId); + queryWrapper.apply(MyBatisUtils.findInSet("label_ids"), pageReqVO.getLabelId()); + return selectPage(pageReqVO, queryWrapper); + } + + /** + * 分页查询文档,并限制在用户可读的知识库、权限和状态范围内 + * + * @param pageReqVO 文档分页查询条件 + * @param readableLibraryIds 用户可读的知识库编号集合 + * @param readablePermissionIds 用户可读的内容权限编号集合 + * @param statuses 文档状态集合 + * @return 文档分页结果 + */ + default PageResult selectPage(PmsKnowledgeDocumentSearchPageReqVO pageReqVO, + Collection readableLibraryIds, + Collection readablePermissionIds, + Collection statuses) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .in(PmsKnowledgeDocumentDO::getLibraryId, readableLibraryIds) + .in(PmsKnowledgeDocumentDO::getPermissionId, readablePermissionIds) + .in(PmsKnowledgeDocumentDO::getStatus, statuses) + .eqIfPresent(PmsKnowledgeDocumentDO::getLibraryId, pageReqVO.getLibraryId()) + .eqIfPresent(PmsKnowledgeDocumentDO::getCreator, + Convert.toStr(pageReqVO.getCreatorUserId())) + .betweenIfPresent(PmsKnowledgeDocumentDO::getUpdateTime, pageReqVO.getUpdateTime()) + .and(StrUtil.isNotBlank(pageReqVO.getKeyword()), + wrapper -> wrapper.like(PmsKnowledgeDocumentDO::getTitle, pageReqVO.getKeyword()) + .or().like(PmsKnowledgeDocumentDO::getContent, pageReqVO.getKeyword())) + .orderByDesc(PmsKnowledgeDocumentDO::getUpdateTime) + .orderByDesc(PmsKnowledgeDocumentDO::getId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeFolderMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeFolderMapper.java new file mode 100644 index 000000000..0ce2306c8 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/content/PmsKnowledgeFolderMapper.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.content; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Mapper +public interface PmsKnowledgeFolderMapper extends BaseMapperX { + + default Set selectExistingPermissionIdSet(Collection permissionIds) { + return convertSet(selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeFolderDO::getPermissionId, permissionIds) + .select(PmsKnowledgeFolderDO::getPermissionId)), PmsKnowledgeFolderDO::getPermissionId); + } + + default List selectListByLibraryIdAndStatus(Long libraryId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeFolderDO::getLibraryId, libraryId) + .eq(PmsKnowledgeFolderDO::getStatus, status) + .orderByAsc(PmsKnowledgeFolderDO::getCreateTime) + .orderByAsc(PmsKnowledgeFolderDO::getId)); + } + + default List selectListByLibraryId(Long libraryId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeFolderDO::getLibraryId, libraryId) + .orderByAsc(PmsKnowledgeFolderDO::getId)); + } + + default List selectListByParentIds(Collection parentIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeFolderDO::getParentId, parentIds)); + } + + default int updateToRestoreByIds(Collection ids, Integer status) { + return update(new LambdaUpdateWrapper() + .set(PmsKnowledgeFolderDO::getStatus, status) + .set(PmsKnowledgeFolderDO::getDeleteUserId, null) + .set(PmsKnowledgeFolderDO::getDeleteTime, null) + .in(PmsKnowledgeFolderDO::getId, ids)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentCommentMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentCommentMapper.java new file mode 100644 index 000000000..afb88ca64 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentCommentMapper.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentCommentDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsKnowledgeDocumentCommentMapper extends BaseMapperX { + + default List selectListByDocumentId(Long documentId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeDocumentCommentDO::getDocumentId, documentId) + .orderByAsc(PmsKnowledgeDocumentCommentDO::getCreateTime) + .orderByAsc(PmsKnowledgeDocumentCommentDO::getId)); + } + + default void deleteByMainId(Long mainId) { + delete(PmsKnowledgeDocumentCommentDO::getMainId, mainId); + } + + default void deleteByDocumentIds(Collection documentIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeDocumentCommentDO::getDocumentId, documentIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentLikeMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentLikeMapper.java new file mode 100644 index 000000000..c44c71ded --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentLikeMapper.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentLikeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsKnowledgeDocumentLikeMapper extends BaseMapperX { + + default PmsKnowledgeDocumentLikeDO selectByDocumentIdAndUserId(Long documentId, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeDocumentLikeDO::getDocumentId, documentId) + .eq(PmsKnowledgeDocumentLikeDO::getUserId, userId)); + } + + default List selectListByDocumentId(Long documentId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeDocumentLikeDO::getDocumentId, documentId) + .orderByAsc(PmsKnowledgeDocumentLikeDO::getCreateTime) + .orderByAsc(PmsKnowledgeDocumentLikeDO::getId)); + } + + default void deleteByDocumentIds(Collection documentIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeDocumentLikeDO::getDocumentId, documentIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentShareMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentShareMapper.java new file mode 100644 index 000000000..f76bb23b7 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeDocumentShareMapper.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentShareDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; + +@Mapper +public interface PmsKnowledgeDocumentShareMapper extends BaseMapperX { + + default PmsKnowledgeDocumentShareDO selectByDocumentId(Long documentId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeDocumentShareDO::getDocumentId, documentId)); + } + + default PmsKnowledgeDocumentShareDO selectByToken(String token) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeDocumentShareDO::getToken, token)); + } + + default int updateToReopen(PmsKnowledgeDocumentShareDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .set(PmsKnowledgeDocumentShareDO::getCloseUserId, null) + .set(PmsKnowledgeDocumentShareDO::getCloseTime, null) + .eq(PmsKnowledgeDocumentShareDO::getId, updateObj.getId())); + } + + default void deleteByDocumentIds(Collection documentIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeDocumentShareDO::getDocumentId, documentIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeFavoriteMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeFavoriteMapper.java new file mode 100644 index 000000000..b392a2f90 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeFavoriteMapper.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoritePageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeFavoriteDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsKnowledgeFavoriteMapper extends BaseMapperX { + + default PageResult selectPage(PmsKnowledgeFavoritePageReqVO pageReqVO, + Long userId, Collection readableFavoriteIds) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(PmsKnowledgeFavoriteDO::getUserId, userId) + .in(PmsKnowledgeFavoriteDO::getId, readableFavoriteIds) + .eqIfPresent(PmsKnowledgeFavoriteDO::getType, pageReqVO.getType()) + .orderByDesc(PmsKnowledgeFavoriteDO::getCreateTime) + .orderByDesc(PmsKnowledgeFavoriteDO::getId)); + } + + default PmsKnowledgeFavoriteDO selectByUserIdAndTypeAndEntityId(Long userId, Integer type, Long entityId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeFavoriteDO::getUserId, userId) + .eq(PmsKnowledgeFavoriteDO::getType, type) + .eq(PmsKnowledgeFavoriteDO::getEntityId, entityId)); + } + + default List selectListByUserId(Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeFavoriteDO::getUserId, userId) + .orderByDesc(PmsKnowledgeFavoriteDO::getCreateTime) + .orderByDesc(PmsKnowledgeFavoriteDO::getId)); + } + + default List selectListByUserIdAndLibraryId(Long userId, Long libraryId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeFavoriteDO::getUserId, userId) + .eq(PmsKnowledgeFavoriteDO::getLibraryId, libraryId) + .orderByDesc(PmsKnowledgeFavoriteDO::getCreateTime) + .orderByDesc(PmsKnowledgeFavoriteDO::getId)); + } + + default List selectListByUserIdAndTypeAndEntityIds( + Long userId, Integer type, Collection entityIds) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeFavoriteDO::getUserId, userId) + .eq(PmsKnowledgeFavoriteDO::getType, type) + .in(PmsKnowledgeFavoriteDO::getEntityId, entityIds)); + } + + default void deleteByLibraryId(Long libraryId) { + delete(PmsKnowledgeFavoriteDO::getLibraryId, libraryId); + } + + default void deleteByTypeAndEntityIds(Collection types, Collection entityIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeFavoriteDO::getType, types) + .in(PmsKnowledgeFavoriteDO::getEntityId, entityIds)); + } + + default void updateLibraryIdByTypeAndEntityIds(Long libraryId, + Collection types, Collection entityIds) { + update(new PmsKnowledgeFavoriteDO().setLibraryId(libraryId), + new LambdaQueryWrapperX() + .in(PmsKnowledgeFavoriteDO::getType, types) + .in(PmsKnowledgeFavoriteDO::getEntityId, entityIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeViewRecordMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeViewRecordMapper.java new file mode 100644 index 000000000..32bafde15 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/interaction/PmsKnowledgeViewRecordMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeViewRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsKnowledgeViewRecordMapper extends BaseMapperX { + + default List selectListByUserIdAndCreateTimeAfter( + Long userId, LocalDateTime beginTime) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeViewRecordDO::getUserId, userId) + .ge(PmsKnowledgeViewRecordDO::getCreateTime, beginTime) + .orderByDesc(PmsKnowledgeViewRecordDO::getCreateTime) + .orderByDesc(PmsKnowledgeViewRecordDO::getId)); + } + + default int updateLibraryIdAndCreateTimeByUserIdAndTypeAndEntityId( + Long libraryId, LocalDateTime createTime, Long userId, Integer type, Long entityId) { + PmsKnowledgeViewRecordDO updateObj = new PmsKnowledgeViewRecordDO().setLibraryId(libraryId); + updateObj.setCreateTime(createTime); + return update(updateObj, + new LambdaQueryWrapperX() + .eq(PmsKnowledgeViewRecordDO::getUserId, userId) + .eq(PmsKnowledgeViewRecordDO::getType, type) + .eq(PmsKnowledgeViewRecordDO::getEntityId, entityId)); + } + + default void deleteByLibraryId(Long libraryId) { + delete(PmsKnowledgeViewRecordDO::getLibraryId, libraryId); + } + + default void deleteByTypeAndEntityIds(Collection types, Collection entityIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeViewRecordDO::getType, types) + .in(PmsKnowledgeViewRecordDO::getEntityId, entityIds)); + } + + default void updateLibraryIdByTypeAndEntityIds(Long libraryId, Collection types, + Collection entityIds) { + update(new PmsKnowledgeViewRecordDO().setLibraryId(libraryId), + new LambdaQueryWrapperX() + .in(PmsKnowledgeViewRecordDO::getType, types) + .in(PmsKnowledgeViewRecordDO::getEntityId, entityIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeGroupMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeGroupMapper.java new file mode 100644 index 000000000..33d5b2972 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeGroupMapper.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.library; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeGroupDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsKnowledgeGroupMapper extends BaseMapperX { + + default PmsKnowledgeGroupDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupDO::getId, id) + .eq(PmsKnowledgeGroupDO::getUserId, userId)); + } + + default PmsKnowledgeGroupDO selectByUserIdAndName(Long userId, String name, Long excludeId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupDO::getUserId, userId) + .eq(PmsKnowledgeGroupDO::getName, name) + .neIfPresent(PmsKnowledgeGroupDO::getId, excludeId)); + } + + default List selectListByUserId(Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupDO::getUserId, userId) + .orderByAsc(PmsKnowledgeGroupDO::getSort) + .orderByAsc(PmsKnowledgeGroupDO::getId)); + } + + default List selectListByIdsAndUserId(Collection ids, Long userId) { + return selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeGroupDO::getId, ids) + .eq(PmsKnowledgeGroupDO::getUserId, userId)); + } + + default PmsKnowledgeGroupDO selectLastByUserId(Long userId) { + return selectLastOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupDO::getUserId, userId) + .orderByAsc(PmsKnowledgeGroupDO::getSort) + .orderByAsc(PmsKnowledgeGroupDO::getId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeGroupRelationMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeGroupRelationMapper.java new file mode 100644 index 000000000..ea30845b9 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeGroupRelationMapper.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeGroupRelationDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Mapper +public interface PmsKnowledgeGroupRelationMapper extends BaseMapperX { + + default PmsKnowledgeGroupRelationDO selectByUserIdAndLibraryId(Long userId, Long libraryId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupRelationDO::getUserId, userId) + .eq(PmsKnowledgeGroupRelationDO::getLibraryId, libraryId)); + } + + default List selectListByUserIdAndLibraryIds( + Long userId, Collection libraryIds) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupRelationDO::getUserId, userId) + .in(PmsKnowledgeGroupRelationDO::getLibraryId, libraryIds)); + } + + default List selectListByUserIdAndGroupId(Long userId, Long groupId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupRelationDO::getUserId, userId) + .eq(PmsKnowledgeGroupRelationDO::getGroupId, groupId)); + } + + /** + * 统计用户在指定知识库集合中各分组的知识库数量,使用 DISTINCT 避免重复关系影响统计 + * + * @param userId 用户编号 + * @param libraryIds 知识库编号集合 + * @return 分组编号 -> 知识库数量 + */ + default Map selectGroupLibraryCountMapByUserIdAndLibraryIds(Long userId, + Collection libraryIds) { + List> rows = selectMaps(new QueryWrapperX() + .select("group_id AS groupId", "COUNT(DISTINCT library_id) AS count") + .eq("user_id", userId) + .in("library_id", libraryIds) + .groupBy("group_id")); + Map groupCountMap = new LinkedHashMap<>(); + for (Map row : rows) { + groupCountMap.put(((Number) row.get("groupId")).longValue(), ((Number) row.get("count")).intValue()); + } + return groupCountMap; + } + + /** + * 统计用户在指定知识库集合中已分组的知识库数量 + * + * @param userId 用户编号 + * @param libraryIds 知识库编号集合 + * @return 已分组知识库数量 + */ + default int selectGroupedLibraryCountByUserIdAndLibraryIds(Long userId, Collection libraryIds) { + Map row = CollUtil.getFirst(selectMaps(new QueryWrapperX() + .select("COUNT(DISTINCT library_id) AS count") + .eq("user_id", userId) + .in("library_id", libraryIds))); + return row == null ? 0 : ((Number) row.get("count")).intValue(); + } + + default void deleteByUserIdAndGroupId(Long userId, Long groupId) { + delete(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupRelationDO::getUserId, userId) + .eq(PmsKnowledgeGroupRelationDO::getGroupId, groupId)); + } + + default void deleteByUserIdAndLibraryId(Long userId, Long libraryId) { + delete(new LambdaQueryWrapperX() + .eq(PmsKnowledgeGroupRelationDO::getUserId, userId) + .eq(PmsKnowledgeGroupRelationDO::getLibraryId, libraryId)); + } + + default void deleteByLibraryId(Long libraryId) { + delete(PmsKnowledgeGroupRelationDO::getLibraryId, libraryId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryMapper.java new file mode 100644 index 000000000..7c7dfee20 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryMapper.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibraryPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Mapper +public interface PmsKnowledgeLibraryMapper extends BaseMapperX { + + default List selectIdList(Boolean openStatus, Integer status) { + return convertList(selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeLibraryDO::getStatus, status) + .eqIfPresent(PmsKnowledgeLibraryDO::getOpenStatus, openStatus) + .select(PmsKnowledgeLibraryDO::getId)), PmsKnowledgeLibraryDO::getId); + } + + /** + * 分页查询知识库,并按公开状态和成员关系过滤可见范围 + * + * @param pageReqVO 分页查询条件 + * @param memberLibraryIds 当前用户作为成员可访问的知识库编号集合 + * @param includeAll 是否包含全部知识库 + * @param filterLibraryIds 指定过滤的知识库编号集合 + * @param status 知识库状态 + * @return 知识库分页结果 + */ + default PageResult selectPage(PmsKnowledgeLibraryPageReqVO pageReqVO, + Collection memberLibraryIds, + boolean includeAll, + Collection filterLibraryIds, + Integer status) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .eq(PmsKnowledgeLibraryDO::getStatus, status) + .likeIfPresent(PmsKnowledgeLibraryDO::getName, pageReqVO.getName()); + if (filterLibraryIds != null) { + query.in(PmsKnowledgeLibraryDO::getId, filterLibraryIds); + } else if (!includeAll) { + query.and(wrapper -> { + wrapper.eq(PmsKnowledgeLibraryDO::getOpenStatus, true); + if (CollUtil.isNotEmpty(memberLibraryIds)) { + wrapper.or().in(PmsKnowledgeLibraryDO::getId, memberLibraryIds); + } + }); + } + query.orderByDesc(PmsKnowledgeLibraryDO::getCreateTime).orderByDesc(PmsKnowledgeLibraryDO::getId); + return selectPage(pageReqVO, query); + } + + default int updateForEdit(PmsKnowledgeLibraryDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .set(updateObj.getDescription() == null, PmsKnowledgeLibraryDO::getDescription, null) + .set(updateObj.getCoverUrl() == null, PmsKnowledgeLibraryDO::getCoverUrl, null) + .eq(PmsKnowledgeLibraryDO::getId, updateObj.getId())); + } + + default int updateToRestoreById(Long id, Integer status) { + return update(new LambdaUpdateWrapper() + .set(PmsKnowledgeLibraryDO::getStatus, status) + .set(PmsKnowledgeLibraryDO::getDeleteUserId, null) + .set(PmsKnowledgeLibraryDO::getDeleteTime, null) + .eq(PmsKnowledgeLibraryDO::getId, id)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryMemberMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryMemberMapper.java new file mode 100644 index 000000000..b976be0d9 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryMemberMapper.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.library; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Mapper +public interface PmsKnowledgeLibraryMemberMapper extends BaseMapperX { + + default PmsKnowledgeLibraryMemberDO selectByLibraryIdAndUserId(Long libraryId, Long userId) { + return selectOne(PmsKnowledgeLibraryMemberDO::getLibraryId, libraryId, + PmsKnowledgeLibraryMemberDO::getUserId, userId); + } + + default PmsKnowledgeLibraryMemberDO selectByLibraryIdAndDeptId(Long libraryId, Long deptId) { + return selectOne(PmsKnowledgeLibraryMemberDO::getLibraryId, libraryId, + PmsKnowledgeLibraryMemberDO::getDeptId, deptId); + } + + default List selectListByDeptIds(Collection deptIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeLibraryMemberDO::getDeptId, deptIds)); + } + + default List selectListByLibraryId(Long libraryId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeLibraryMemberDO::getLibraryId, libraryId) + .orderByAsc(PmsKnowledgeLibraryMemberDO::getLevel) + .orderByAsc(PmsKnowledgeLibraryMemberDO::getId)); + } + + default List selectListByLibraryIds(Collection libraryIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsKnowledgeLibraryMemberDO::getLibraryId, libraryIds) + .orderByAsc(PmsKnowledgeLibraryMemberDO::getLibraryId) + .orderByAsc(PmsKnowledgeLibraryMemberDO::getLevel) + .orderByAsc(PmsKnowledgeLibraryMemberDO::getId)); + } + + default List selectLibraryIdListByUserIdOrDeptId(Long userId, Long deptId) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX<>(); + query.eq(PmsKnowledgeLibraryMemberDO::getUserId, userId); + if (deptId != null) { + query.or().eq(PmsKnowledgeLibraryMemberDO::getDeptId, deptId); + } + return convertList(selectList(query), PmsKnowledgeLibraryMemberDO::getLibraryId); + } + + default void deleteByLibraryIdAndLevels(Long libraryId, Collection levels) { + delete(new LambdaQueryWrapperX() + .eq(PmsKnowledgeLibraryMemberDO::getLibraryId, libraryId) + .in(PmsKnowledgeLibraryMemberDO::getLevel, levels)); + } + + default void deleteByLibraryId(Long libraryId) { + delete(PmsKnowledgeLibraryMemberDO::getLibraryId, libraryId); + } + + default void deleteByLibraryIdAndUserId(Long libraryId, Long userId) { + delete(new LambdaQueryWrapperX() + .eq(PmsKnowledgeLibraryMemberDO::getLibraryId, libraryId) + .eq(PmsKnowledgeLibraryMemberDO::getUserId, userId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryTemplateMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryTemplateMapper.java new file mode 100644 index 000000000..e33978c9f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/library/PmsKnowledgeLibraryTemplateMapper.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.library; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplatePageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PmsKnowledgeLibraryTemplateMapper extends BaseMapperX { + + default PageResult selectPage(PmsKnowledgeLibraryTemplatePageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(PmsKnowledgeLibraryTemplateDO::getName, pageReqVO.getName()) + .eq(pageReqVO.getStatus() != null, PmsKnowledgeLibraryTemplateDO::getStatus, pageReqVO.getStatus()) + .orderByAsc(PmsKnowledgeLibraryTemplateDO::getSort) + .orderByDesc(PmsKnowledgeLibraryTemplateDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(PmsKnowledgeLibraryTemplateDO::getStatus, status) + .orderByAsc(PmsKnowledgeLibraryTemplateDO::getSort) + .orderByDesc(PmsKnowledgeLibraryTemplateDO::getId)); + } + + default PmsKnowledgeLibraryTemplateDO selectByName(String name) { + return selectOne(PmsKnowledgeLibraryTemplateDO::getName, name); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/recycle/PmsKnowledgeRecycleRecordMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/recycle/PmsKnowledgeRecycleRecordMapper.java new file mode 100644 index 000000000..5315a6b37 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/kb/recycle/PmsKnowledgeRecycleRecordMapper.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.kb.recycle; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.recycle.PmsKnowledgeRecycleRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsKnowledgeRecycleRecordMapper extends BaseMapperX { + + default PmsKnowledgeRecycleRecordDO selectByTypeAndEntityId(Integer type, Long entityId) { + return selectOne(PmsKnowledgeRecycleRecordDO::getType, type, + PmsKnowledgeRecycleRecordDO::getEntityId, entityId); + } + + default List selectListByLibraryId(Long libraryId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeRecycleRecordDO::getLibraryId, libraryId) + .orderByDesc(PmsKnowledgeRecycleRecordDO::getDeleteTime) + .orderByDesc(PmsKnowledgeRecycleRecordDO::getId)); + } + + default List selectListByTypeAndDeleteUserId(Integer type, Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsKnowledgeRecycleRecordDO::getType, type) + .eq(PmsKnowledgeRecycleRecordDO::getDeleteUserId, userId) + .orderByDesc(PmsKnowledgeRecycleRecordDO::getDeleteTime) + .orderByDesc(PmsKnowledgeRecycleRecordDO::getId)); + } + + default List selectListByDeleteTimeBefore(LocalDateTime deleteTime) { + return selectList(new LambdaQueryWrapperX() + .le(PmsKnowledgeRecycleRecordDO::getDeleteTime, deleteTime) + .orderByAsc(PmsKnowledgeRecycleRecordDO::getDeleteTime) + .orderByAsc(PmsKnowledgeRecycleRecordDO::getId)); + } + + default void deleteByLibraryId(Long libraryId) { + delete(PmsKnowledgeRecycleRecordDO::getLibraryId, libraryId); + } + + default void deleteByTypeAndEntityIds(Collection types, Collection entityIds) { + delete(new LambdaQueryWrapperX() + .in(PmsKnowledgeRecycleRecordDO::getType, types) + .in(PmsKnowledgeRecycleRecordDO::getEntityId, entityIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/package-info.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/package-info.java new file mode 100644 index 000000000..ce46c8e6f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/package-info.java @@ -0,0 +1,6 @@ +/** + * PMS 模块的数据访问层,提供 MySQL 数据访问实现 + * + * 其中,MySQL 的表以 pms_ 作为前缀 + */ +package cn.iocoder.yudao.module.pms.dal.mysql; diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/iteration/PmsIterationMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/iteration/PmsIterationMapper.java new file mode 100644 index 000000000..8625f0ed0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/iteration/PmsIterationMapper.java @@ -0,0 +1,122 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.iteration; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Mapper +public interface PmsIterationMapper extends BaseMapperX { + + default PageResult selectPage(PmsIterationPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(PmsIterationDO::getProjectId, pageReqVO.getProjectId()) + .likeIfPresent(PmsIterationDO::getName, pageReqVO.getName()) + .eqIfPresent(PmsIterationDO::getStatus, pageReqVO.getStatus()) + .orderByAsc(PmsIterationDO::getSort) + .orderByDesc(PmsIterationDO::getId)); + } + + default Integer selectMaxSortByProjectId(Long projectId) { + PmsIterationDO iteration = selectLastOne(new LambdaQueryWrapperX() + .select(PmsIterationDO::getSort) + .eq(PmsIterationDO::getProjectId, projectId) + .orderByAsc(PmsIterationDO::getSort) + .orderByAsc(PmsIterationDO::getId)); + return iteration != null ? iteration.getSort() : null; + } + + default List selectListByProjectId(Long projectId) { + return selectList(PmsIterationDO::getProjectId, projectId); + } + + /** + * 分页查询当前用户负责的迭代,用于工作台展示 + * + * @param pageParam 分页参数 + * @param projectIds 当前用户可访问的项目编号集合 + * @param projectId 项目编号,可选 + * @param name 迭代名称,可选 + * @param status 迭代状态,可选 + * @param endTime 结束时间范围,可选 + * @param completedStatus 已完成状态 + * @param ownerUserId 迭代负责人编号 + * @return 迭代分页结果 + */ + default PageResult selectWorkbenchPage(PageParam pageParam, Collection projectIds, + Long projectId, String name, + Integer status, LocalDateTime[] endTime, + Integer completedStatus, Long ownerUserId) { + return selectPage(pageParam, new LambdaQueryWrapperX() + .in(PmsIterationDO::getProjectId, projectIds) + .eqIfPresent(PmsIterationDO::getProjectId, projectId) + .eq(PmsIterationDO::getOwnerUserId, ownerUserId) + .likeIfPresent(PmsIterationDO::getName, name) + .eqIfPresent(PmsIterationDO::getStatus, status) + .betweenIfPresent(PmsIterationDO::getEndTime, endTime) + .ne(status == null, PmsIterationDO::getStatus, completedStatus) + .orderByAsc(PmsIterationDO::getEndTime) + .orderByDesc(PmsIterationDO::getId)); + } + + /** + * 统计当前用户负责的迭代数量,用于工作台统计 + * + * @param projectIds 当前用户可访问的项目编号集合 + * @param projectId 项目编号,可选 + * @param name 迭代名称,可选 + * @param status 迭代状态,可选 + * @param endTime 结束时间范围,可选 + * @param completedStatus 已完成状态 + * @param ownerUserId 迭代负责人编号 + * @return 迭代数量 + */ + default Long selectWorkbenchCount(Collection projectIds, Long projectId, String name, + Integer status, LocalDateTime[] endTime, + Integer completedStatus, Long ownerUserId) { + return selectCount(new LambdaQueryWrapperX() + .in(PmsIterationDO::getProjectId, projectIds) + .eqIfPresent(PmsIterationDO::getProjectId, projectId) + .eq(PmsIterationDO::getOwnerUserId, ownerUserId) + .likeIfPresent(PmsIterationDO::getName, name) + .eqIfPresent(PmsIterationDO::getStatus, status) + .betweenIfPresent(PmsIterationDO::getEndTime, endTime) + .ne(status == null, PmsIterationDO::getStatus, completedStatus)); + } + + /** + * 统计各项目下不同状态的迭代数量 + * + * @param projectIds 项目编号集合 + * @return 项目编号 ->(状态 -> 迭代数量) + */ + default Map> selectStatusCountMapByProjectIds(Collection projectIds) { + List> rows = selectMaps(new QueryWrapperX() + .select("project_id AS projectId", "status", "COUNT(*) AS count") + .in("project_id", projectIds) + .groupBy("project_id", "status")); + Map> projectStatusCountMap = new LinkedHashMap<>(); + for (Map row : rows) { + Long projectId = ((Number) row.get("projectId")).longValue(); + Integer status = ((Number) row.get("status")).intValue(); + projectStatusCountMap.computeIfAbsent(projectId, key -> new LinkedHashMap<>()) + .put(status, ((Number) row.get("count")).longValue()); + } + return projectStatusCountMap; + } + + default void deleteByProjectId(Long projectId) { + delete(PmsIterationDO::getProjectId, projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectAnnouncementMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectAnnouncementMapper.java new file mode 100644 index 000000000..ae627fff6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectAnnouncementMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectAnnouncementDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PmsProjectAnnouncementMapper extends BaseMapperX { + + default List selectListByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsProjectAnnouncementDO::getProjectId, projectId) + .orderByDesc(PmsProjectAnnouncementDO::getCreateTime) + .orderByDesc(PmsProjectAnnouncementDO::getId)); + } + + default void deleteByProjectId(Long projectId) { + delete(new LambdaQueryWrapperX() + .eq(PmsProjectAnnouncementDO::getProjectId, projectId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectFavoriteMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectFavoriteMapper.java new file mode 100644 index 000000000..c86232a0c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectFavoriteMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectFavoriteDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Mapper +public interface PmsProjectFavoriteMapper extends BaseMapperX { + + default PmsProjectFavoriteDO selectByProjectIdAndUserId(Long projectId, Long userId) { + return selectFirstOne(PmsProjectFavoriteDO::getProjectId, projectId, + PmsProjectFavoriteDO::getUserId, userId); + } + + default List selectProjectIdListByUserId(Long userId) { + return convertList(selectList(new LambdaQueryWrapperX() + .eq(PmsProjectFavoriteDO::getUserId, userId) + .orderByDesc(PmsProjectFavoriteDO::getCreateTime)), PmsProjectFavoriteDO::getProjectId); + } + + default void deleteByProjectIds(Collection projectIds) { + delete(new LambdaQueryWrapperX() + .in(PmsProjectFavoriteDO::getProjectId, projectIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectGroupMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectGroupMapper.java new file mode 100644 index 000000000..6caf0e01d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectGroupMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectGroupDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsProjectGroupMapper extends BaseMapperX { + + default PmsProjectGroupDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsProjectGroupDO::getId, id) + .eq(PmsProjectGroupDO::getUserId, userId)); + } + + default PmsProjectGroupDO selectByUserIdAndType(Long userId, Integer type) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsProjectGroupDO::getUserId, userId) + .eq(PmsProjectGroupDO::getType, type)); + } + + default PmsProjectGroupDO selectByUserIdAndName(Long userId, String name, Long excludeId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsProjectGroupDO::getUserId, userId) + .eq(PmsProjectGroupDO::getName, name) + .neIfPresent(PmsProjectGroupDO::getId, excludeId)); + } + + default List selectListByUserId(Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsProjectGroupDO::getUserId, userId) + .orderByAsc(PmsProjectGroupDO::getSort) + .orderByAsc(PmsProjectGroupDO::getId)); + } + + default List selectListByIdsAndUserId(Collection ids, Long userId) { + return selectList(new LambdaQueryWrapperX() + .in(PmsProjectGroupDO::getId, ids) + .eq(PmsProjectGroupDO::getUserId, userId)); + } + + default PmsProjectGroupDO selectLastByUserId(Long userId) { + return selectLastOne(new LambdaQueryWrapperX() + .eq(PmsProjectGroupDO::getUserId, userId) + .orderByAsc(PmsProjectGroupDO::getSort) + .orderByAsc(PmsProjectGroupDO::getId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectGroupRelationMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectGroupRelationMapper.java new file mode 100644 index 000000000..7e52dc877 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectGroupRelationMapper.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectGroupRelationDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Mapper +public interface PmsProjectGroupRelationMapper extends BaseMapperX { + + default PmsProjectGroupRelationDO selectByUserIdAndProjectId(Long userId, Long projectId) { + return selectFirstOne(PmsProjectGroupRelationDO::getUserId, userId, + PmsProjectGroupRelationDO::getProjectId, projectId); + } + + default List selectListByUserIdAndProjectIds( + Long userId, Collection projectIds) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsProjectGroupRelationDO::getUserId, userId) + .in(PmsProjectGroupRelationDO::getProjectId, projectIds)); + } + + default List selectListByUserIdAndGroupId(Long userId, Long groupId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsProjectGroupRelationDO::getUserId, userId) + .eq(PmsProjectGroupRelationDO::getGroupId, groupId)); + } + + /** + * 统计用户在指定项目集合中各分组的项目数量,使用 DISTINCT 避免重复关系影响统计 + * + * @param userId 用户编号 + * @param projectIds 项目编号集合 + * @return 分组编号 -> 项目数量 + */ + default Map selectGroupCountMapByUserIdAndProjectIds(Long userId, Collection projectIds) { + List> rows = selectMaps(new QueryWrapperX() + .select("group_id AS groupId", "COUNT(DISTINCT project_id) AS count") + .eq("user_id", userId) + .in("project_id", projectIds) + .groupBy("group_id")); + Map groupCountMap = new LinkedHashMap<>(); + for (Map row : rows) { + groupCountMap.put(((Number) row.get("groupId")).longValue(), + ((Number) row.get("count")).intValue()); + } + return groupCountMap; + } + + /** + * 统计用户在指定项目集合中已分组的项目数量 + * + * @param userId 用户编号 + * @param projectIds 项目编号集合 + * @return 已分组项目数量 + */ + default int selectProjectCountByUserIdAndProjectIds(Long userId, Collection projectIds) { + Map row = CollUtil.getFirst(selectMaps(new QueryWrapperX() + .select("COUNT(DISTINCT project_id) AS count") + .eq("user_id", userId) + .in("project_id", projectIds))); + return row == null ? 0 : ((Number) row.get("count")).intValue(); + } + + default void deleteByUserIdAndGroupId(Long userId, Long groupId) { + delete(new LambdaQueryWrapperX() + .eq(PmsProjectGroupRelationDO::getUserId, userId) + .eq(PmsProjectGroupRelationDO::getGroupId, groupId)); + } + + default void deleteByUserIdAndProjectId(Long userId, Long projectId) { + delete(new LambdaQueryWrapperX() + .eq(PmsProjectGroupRelationDO::getUserId, userId) + .eq(PmsProjectGroupRelationDO::getProjectId, projectId)); + } + + default void deleteByProjectId(Long projectId) { + delete(PmsProjectGroupRelationDO::getProjectId, projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectMapper.java new file mode 100644 index 000000000..5b4a5e15b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectMapper.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectSortTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsProjectMapper extends BaseMapperX { + + default List selectListByIdsAndStatus(Collection ids, Integer status) { + return selectList(new LambdaQueryWrapperX() + .in(PmsProjectDO::getId, ids) + .eq(PmsProjectDO::getStatus, status) + .orderByDesc(PmsProjectDO::getAccessTime) + .orderByDesc(PmsProjectDO::getId)); + } + + /** + * 分页查询项目,并按访问范围过滤项目 + * + * @param pageReqVO 分页查询条件 + * @param projectIds 当前用户可访问的项目编号集合 + * @param includeOpenProject 是否包含公开项目 + * @param includeAllProject 是否包含全部项目 + * @return 项目分页结果 + */ + default PageResult selectPage(PmsProjectPageReqVO pageReqVO, Collection projectIds, + boolean includeOpenProject, boolean includeAllProject) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(PmsProjectDO::getName, pageReqVO.getName()) + .eq(PmsProjectDO::getStatus, pageReqVO.getStatus()); + if (!includeAllProject) { + if (includeOpenProject) { + query.and(wrapper -> { + wrapper.eq(PmsProjectDO::getOpenStatus, true); + if (CollUtil.isNotEmpty(projectIds)) { + wrapper.or().in(PmsProjectDO::getId, projectIds); + } + }); + } else { + query.in(PmsProjectDO::getId, projectIds); + } + } + if (PmsProjectSortTypeEnum.ACCESS_TIME.getType().equals(pageReqVO.getSortType())) { + query.orderByDesc(PmsProjectDO::getAccessTime); + } else { + query.orderByAsc(PmsProjectDO::getCreateTime); + } + query.orderByDesc(PmsProjectDO::getId); + return selectPage(pageReqVO, query); + } + + default int updateStatusAndArchiveTimeById(Long id, Integer sourceStatus, Integer status, + LocalDateTime archiveTime) { + return update(new LambdaUpdateWrapper().eq(PmsProjectDO::getId, id) + .eq(PmsProjectDO::getStatus, sourceStatus) + .set(PmsProjectDO::getStatus, status) + .set(PmsProjectDO::getArchiveTime, archiveTime)); + } + + default int updateStatusAndRecycleTimeById(Long id, Integer sourceStatus, Integer status, + LocalDateTime recycleTime) { + return update(new LambdaUpdateWrapper().eq(PmsProjectDO::getId, id) + .eq(PmsProjectDO::getStatus, sourceStatus) + .set(PmsProjectDO::getStatus, status) + .set(PmsProjectDO::getRecycleTime, recycleTime)); + } + + default int updateToRestoreById(Long id, Integer status) { + return update(new LambdaUpdateWrapper().eq(PmsProjectDO::getId, id) + .in(PmsProjectDO::getStatus, Arrays.asList(PmsProjectStatusEnum.ARCHIVED.getStatus(), + PmsProjectStatusEnum.RECYCLED.getStatus())) + .set(PmsProjectDO::getStatus, status) + .set(PmsProjectDO::getArchiveTime, null) + .set(PmsProjectDO::getRecycleTime, null)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectMemberMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectMemberMapper.java new file mode 100644 index 000000000..0f35d229c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectMemberMapper.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.project; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectMemberDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsProjectMemberMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(PmsProjectMemberDO::getUserId, userId); + } + + default List selectListByProjectId(Long projectId) { + return selectList(PmsProjectMemberDO::getProjectId, projectId); + } + + default PmsProjectMemberDO selectByProjectIdAndUserId(Long projectId, Long userId) { + return selectFirstOne(PmsProjectMemberDO::getProjectId, projectId, + PmsProjectMemberDO::getUserId, userId); + } + + default List selectListByUserIdAndLevels(Long userId, Collection levels) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsProjectMemberDO::getUserId, userId) + .in(PmsProjectMemberDO::getLevel, levels)); + } + + default List selectListByProjectIds(Collection projectIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsProjectMemberDO::getProjectId, projectIds)); + } + + default List selectListByProjectIdsAndLevels(Collection projectIds, + Collection levels) { + return selectList(new LambdaQueryWrapperX() + .in(PmsProjectMemberDO::getProjectId, projectIds) + .in(PmsProjectMemberDO::getLevel, levels)); + } + + default void deleteByProjectIdAndUserIds(Long projectId, Collection userIds) { + delete(new LambdaQueryWrapperX() + .eq(PmsProjectMemberDO::getProjectId, projectId) + .in(PmsProjectMemberDO::getUserId, userIds)); + } + + default void deleteByProjectIdAndUserId(Long projectId, Long userId) { + delete(new LambdaQueryWrapperX() + .eq(PmsProjectMemberDO::getProjectId, projectId) + .eq(PmsProjectMemberDO::getUserId, userId)); + } + + default void deleteByProjectId(Long projectId) { + delete(PmsProjectMemberDO::getProjectId, projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectTemplateMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectTemplateMapper.java new file mode 100644 index 000000000..dd9cafe6b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/project/PmsProjectTemplateMapper.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.project; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplatePageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PmsProjectTemplateMapper extends BaseMapperX { + + default PageResult selectPage(PmsProjectTemplatePageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(PmsProjectTemplateDO::getName, pageReqVO.getName()) + .eqIfPresent(PmsProjectTemplateDO::getProjectType, pageReqVO.getProjectType()) + .eqIfPresent(PmsProjectTemplateDO::getStatus, pageReqVO.getStatus()) + .orderByAsc(PmsProjectTemplateDO::getSort) + .orderByDesc(PmsProjectTemplateDO::getId)); + } + + default PmsProjectTemplateDO selectByNameAndProjectType(String name, Integer projectType) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsProjectTemplateDO::getName, name) + .eq(PmsProjectTemplateDO::getProjectType, projectType)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemActivityMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemActivityMapper.java new file mode 100644 index 000000000..9f40a25ac --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemActivityMapper.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemActivityDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsWorkItemActivityMapper extends BaseMapperX { + + default List selectListByWorkItemId(Long workItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemActivityDO::getWorkItemId, workItemId) + .orderByDesc(PmsWorkItemActivityDO::getCreateTime) + .orderByDesc(PmsWorkItemActivityDO::getId)); + } + + default List selectListByWorkItemIds(Collection workItemIds, int limit) { + return selectList(new LambdaQueryWrapperX() + .in(PmsWorkItemActivityDO::getWorkItemId, workItemIds) + .orderByDesc(PmsWorkItemActivityDO::getCreateTime) + .orderByDesc(PmsWorkItemActivityDO::getId) + .last("LIMIT " + limit)); + } + + default void deleteByWorkItemId(Long workItemId) { + delete(PmsWorkItemActivityDO::getWorkItemId, workItemId); + } + + default void deleteByProjectId(Long projectId) { + delete(PmsWorkItemActivityDO::getProjectId, projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemBoardMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemBoardMapper.java new file mode 100644 index 000000000..3c776cab2 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemBoardMapper.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PmsWorkItemBoardMapper extends BaseMapperX { + + default List selectListByProjectIdAndWorkItemType(Long projectId, Integer workItemType) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemBoardDO::getProjectId, projectId) + .eq(PmsWorkItemBoardDO::getWorkItemType, workItemType) + .orderByAsc(PmsWorkItemBoardDO::getSort) + .orderByAsc(PmsWorkItemBoardDO::getId)); + } + + default void deleteByProjectIdAndWorkItemType(Long projectId, Integer workItemType) { + delete(new LambdaQueryWrapperX() + .eq(PmsWorkItemBoardDO::getProjectId, projectId) + .eq(PmsWorkItemBoardDO::getWorkItemType, workItemType)); + } + + default void deleteByProjectId(Long projectId) { + delete(PmsWorkItemBoardDO::getProjectId, projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemCommentMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemCommentMapper.java new file mode 100644 index 000000000..950994b83 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemCommentMapper.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemCommentDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsWorkItemCommentMapper extends BaseMapperX { + + default List selectListByWorkItemId(Long workItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemCommentDO::getWorkItemId, workItemId) + .orderByAsc(PmsWorkItemCommentDO::getCreateTime) + .orderByAsc(PmsWorkItemCommentDO::getId)); + } + + default void deleteByMainId(Long mainId) { + delete(PmsWorkItemCommentDO::getMainId, mainId); + } + + default void deleteByWorkItemId(Long workItemId) { + delete(PmsWorkItemCommentDO::getWorkItemId, workItemId); + } + + default void deleteByWorkItemIds(Collection workItemIds) { + delete(new LambdaQueryWrapperX() + .in(PmsWorkItemCommentDO::getWorkItemId, workItemIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemLabelMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemLabelMapper.java new file mode 100644 index 000000000..2dac02713 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemLabelMapper.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PmsWorkItemLabelMapper extends BaseMapperX { + + default List selectListByName(String name) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(PmsWorkItemLabelDO::getName, name) + .orderByAsc(PmsWorkItemLabelDO::getCreateTime) + .orderByAsc(PmsWorkItemLabelDO::getId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemMapper.java new file mode 100644 index 000000000..ad9927230 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemMapper.java @@ -0,0 +1,394 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemLifecycleStatusEnum; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Mapper +public interface PmsWorkItemMapper extends BaseMapperX { + + default PageResult selectPage(PmsWorkItemPageReqVO pageReqVO) { + LambdaQueryWrapperX queryWrapper = buildQueryWrapper(pageReqVO); + if (Boolean.TRUE.equals(pageReqVO.getPlanningOnly())) { + queryWrapper.isNull(PmsWorkItemDO::getParentId) + .orderByAsc(PmsWorkItemDO::getSort) + .orderByDesc(PmsWorkItemDO::getId); + } else { + queryWrapper.orderByDesc(PmsWorkItemDO::getPriority) + .orderByAsc(PmsWorkItemDO::getSerialNumber) + .orderByAsc(PmsWorkItemDO::getId); + } + return selectPage(pageReqVO, queryWrapper); + } + + default List selectRootList(PmsWorkItemPageReqVO pageReqVO) { + return selectList(buildQueryWrapper(pageReqVO) + .isNull(PmsWorkItemDO::getParentId) + .orderByAsc(PmsWorkItemDO::getSort) + .orderByDesc(PmsWorkItemDO::getId)); + } + + default List selectList(PmsWorkItemPageReqVO pageReqVO) { + LambdaQueryWrapperX queryWrapper = buildQueryWrapper(pageReqVO); + if (Boolean.TRUE.equals(pageReqVO.getRootOnly())) { + queryWrapper.isNull(PmsWorkItemDO::getParentId); + } + return selectList(queryWrapper.orderByDesc(PmsWorkItemDO::getPriority) + .orderByAsc(PmsWorkItemDO::getSerialNumber) + .orderByAsc(PmsWorkItemDO::getId)); + } + + default Integer selectMaxSerialNumberByProjectId(Long projectId) { + PmsWorkItemDO workItem = selectLastOne(new LambdaQueryWrapperX() + .select(PmsWorkItemDO::getSerialNumber) + .eq(PmsWorkItemDO::getProjectId, projectId) + .orderByAsc(PmsWorkItemDO::getSerialNumber) + .orderByAsc(PmsWorkItemDO::getId)); + return workItem != null ? workItem.getSerialNumber() : null; + } + + /** + * 统计各项目下不同工作项状态的数量,仅统计生命周期正常的工作项 + * + * @param projectIds 项目编号集合 + * @return 项目编号 ->(状态 -> 工作项数量) + */ + default Map> selectStatusCountMapByProjectIds(Collection projectIds) { + List> rows = selectMaps(new QueryWrapperX() + .select("project_id AS projectId", "status", "COUNT(*) AS count") + .in("project_id", projectIds) + .eq("lifecycle_status", PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .groupBy("project_id", "status")); + Map> projectStatusCountMap = new LinkedHashMap<>(); + for (Map row : rows) { + Long projectId = ((Number) row.get("projectId")).longValue(); + Integer status = ((Number) row.get("status")).intValue(); + projectStatusCountMap.computeIfAbsent(projectId, key -> new LinkedHashMap<>()) + .put(status, ((Number) row.get("count")).longValue()); + } + return projectStatusCountMap; + } + + /** + * 统计各迭代下不同语义状态的工作项数量 + * + * @param iterationIds 迭代编号集合 + * @return 迭代编号 ->(语义状态 -> 工作项数量) + */ + default Map> selectStatusCountMapByIterationIds(Collection iterationIds) { + List> rows = selectMaps(new QueryWrapperX() + .select("iteration_id AS iterationId", "status", "COUNT(*) AS count") + .in("iteration_id", iterationIds) + .eq("lifecycle_status", PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .groupBy("iteration_id", "status")); + Map> iterationStatusCountMap = new LinkedHashMap<>(); + for (Map row : rows) { + Long iterationId = ((Number) row.get("iterationId")).longValue(); + Integer status = ((Number) row.get("status")).intValue(); + iterationStatusCountMap.computeIfAbsent(iterationId, key -> new LinkedHashMap<>()) + .put(status, ((Number) row.get("count")).longValue()); + } + return iterationStatusCountMap; + } + + default Integer selectMaxSortByStatusId(Long statusId) { + PmsWorkItemDO workItem = selectLastOne(new LambdaQueryWrapperX() + .select(PmsWorkItemDO::getSort) + .eq(PmsWorkItemDO::getStatusId, statusId) + .orderByAsc(PmsWorkItemDO::getSort) + .orderByAsc(PmsWorkItemDO::getId)); + return workItem != null ? workItem.getSort() : null; + } + + default List selectListByStatusId(Long statusId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemDO::getStatusId, statusId) + .eq(PmsWorkItemDO::getLifecycleStatus, PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .isNull(PmsWorkItemDO::getParentId) + .orderByAsc(PmsWorkItemDO::getSort) + .orderByAsc(PmsWorkItemDO::getId)); + } + + default List selectListByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemDO::getProjectId, projectId) + .orderByDesc(PmsWorkItemDO::getId)); + } + + default List selectActiveListByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemDO::getProjectId, projectId) + .eq(PmsWorkItemDO::getLifecycleStatus, PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .orderByDesc(PmsWorkItemDO::getId)); + } + + default List selectListByIterationId(Long iterationId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemDO::getIterationId, iterationId) + .eq(PmsWorkItemDO::getLifecycleStatus, PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .orderByDesc(PmsWorkItemDO::getId)); + } + + default List selectPlanningList(Long projectId, Long iterationId) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + queryWrapper.eq(PmsWorkItemDO::getProjectId, projectId) + .eq(PmsWorkItemDO::getLifecycleStatus, PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .isNull(PmsWorkItemDO::getParentId); + if (iterationId == null) { + queryWrapper.isNull(PmsWorkItemDO::getIterationId); + } else { + queryWrapper.eq(PmsWorkItemDO::getIterationId, iterationId); + } + return selectList(queryWrapper.orderByAsc(PmsWorkItemDO::getSort).orderByDesc(PmsWorkItemDO::getId)); + } + + /** + * 查询待规划的根工作项列表 + * + * @param pageReqVO 工作项筛选参数 + * @return 待规划工作项列表 + */ + default List selectListByPlanning(PmsWorkItemPageReqVO pageReqVO) { + return selectList(buildQueryWrapper(pageReqVO) + .isNull(PmsWorkItemDO::getParentId) + .orderByAsc(PmsWorkItemDO::getSort) + .orderByDesc(PmsWorkItemDO::getId)); + } + + /** + * 分页查询当前用户负责的工作项,用于工作台展示 + * + * @param pageParam 分页参数 + * @param projectIds 当前用户可访问的项目编号集合 + * @param userId 当前用户编号 + * @param projectId 项目编号,可选 + * @param type 工作项类型,可选 + * @param name 工作项名称或编号,可选 + * @param status 工作项状态,可选 + * @param priority 优先级,可选 + * @param iterationId 迭代编号,可选 + * @param endTime 结束时间范围,可选 + * @param completedStatus 已完成状态 + * @return 工作项分页结果 + */ + default PageResult selectWorkbenchPage(PageParam pageParam, Collection projectIds, + Long userId, Long projectId, Integer type, String name, + Integer status, Integer priority, Long iterationId, + LocalDateTime[] endTime, + Integer completedStatus) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .in(PmsWorkItemDO::getProjectId, projectIds) + .eq(PmsWorkItemDO::getAssigneeUserId, userId) + .eq(PmsWorkItemDO::getLifecycleStatus, PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .eqIfPresent(PmsWorkItemDO::getProjectId, projectId) + .eqIfPresent(PmsWorkItemDO::getType, type) + .eqIfPresent(PmsWorkItemDO::getStatus, status) + .eqIfPresent(PmsWorkItemDO::getPriority, priority) + .eqIfPresent(PmsWorkItemDO::getIterationId, iterationId) + .betweenIfPresent(PmsWorkItemDO::getEndTime, endTime); + queryWrapper.ne(status == null, PmsWorkItemDO::getStatus, completedStatus); + appendNameOrSerialNumberQuery(queryWrapper, name); + return selectPage(pageParam, queryWrapper + .orderByAsc(PmsWorkItemDO::getEndTime) + .orderByDesc(PmsWorkItemDO::getId)); + } + + /** + * 统计工作台当前筛选条件下各工作项类型的数量 + * + * @param pageReqVO 工作台筛选条件 + * @param projectIds 可访问的项目编号集合 + * @param userId 当前用户编号 + * @param lifecycleStatus 工作项生命周期状态 + * @param completedStatus 已完成状态 + * @return 工作项类型 -> 工作项数量 + */ + default Map selectWorkbenchTypeCountMap(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long userId, + Integer lifecycleStatus, Integer completedStatus) { + QueryWrapperX queryWrapper = new QueryWrapperX() + .in("project_id", projectIds) + .eq("assignee_user_id", userId) + .eq("lifecycle_status", lifecycleStatus) + .eqIfPresent("project_id", pageReqVO.getProjectId()) + .eqIfPresent("status", pageReqVO.getStatus()) + .eqIfPresent("priority", pageReqVO.getPriority()) + .eqIfPresent("iteration_id", pageReqVO.getIterationId()) + .betweenIfPresent("end_time", pageReqVO.getEndTime()); + queryWrapper.ne(pageReqVO.getStatus() == null, "status", completedStatus); + appendNameOrSerialNumberQuery(queryWrapper, pageReqVO.getName()); + queryWrapper.select("type", "COUNT(*) AS count").groupBy("type"); + List> rows = selectMaps(queryWrapper); + Map typeCountMap = new LinkedHashMap<>(); + for (Map row : rows) { + typeCountMap.put(((Number) row.get("type")).intValue(), ((Number) row.get("count")).longValue()); + } + return typeCountMap; + } + + default Long selectCountByStatusId(Long statusId) { + return selectCount(PmsWorkItemDO::getStatusId, statusId); + } + + default void updateStatusBySourceStatusId(Long sourceStatusId, Long targetStatusId, Integer targetStatus) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemDO::getStatusId, targetStatusId) + .set(PmsWorkItemDO::getStatus, targetStatus) + .eq(PmsWorkItemDO::getStatusId, sourceStatusId)); + } + + default void updateStatusTypeByStatusId(Long statusId, Integer statusType) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemDO::getStatus, statusType) + .eq(PmsWorkItemDO::getStatusId, statusId)); + } + + default void updateStatusAndSortById(Long id, Long statusId, Integer status, Integer sort) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemDO::getStatusId, statusId) + .set(PmsWorkItemDO::getStatus, status) + .set(PmsWorkItemDO::getSort, sort) + .eq(PmsWorkItemDO::getId, id)); + } + + default int updateForEdit(PmsWorkItemDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .set(updateObj.getDescription() == null, PmsWorkItemDO::getDescription, null) + .set(updateObj.getAssigneeUserId() == null, PmsWorkItemDO::getAssigneeUserId, null) + .set(updateObj.getIterationId() == null, PmsWorkItemDO::getIterationId, null) + .set(updateObj.getParentId() == null, PmsWorkItemDO::getParentId, null) + .set(updateObj.getRelatedRequirementId() == null, PmsWorkItemDO::getRelatedRequirementId, null) + .set(updateObj.getDefectType() == null, PmsWorkItemDO::getDefectType, null) + .set(updateObj.getStartTime() == null, PmsWorkItemDO::getStartTime, null) + .set(updateObj.getEndTime() == null, PmsWorkItemDO::getEndTime, null) + .set(updateObj.getEstimatedHours() == null, PmsWorkItemDO::getEstimatedHours, null) + .set(updateObj.getFileUrls() == null, PmsWorkItemDO::getFileUrls, null) + .set(updateObj.getLabelIds() == null, PmsWorkItemDO::getLabelIds, null) + .eq(PmsWorkItemDO::getId, updateObj.getId())); + } + + default void updateIterationIdToNullByIterationId(Long iterationId) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemDO::getIterationId, null) + .eq(PmsWorkItemDO::getIterationId, iterationId)); + } + + default void updateParentIdToNullByParentId(Long parentId) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemDO::getParentId, null) + .eq(PmsWorkItemDO::getParentId, parentId)); + } + + default void updateRelatedRequirementIdToNullByRelatedRequirementId(Long relatedRequirementId) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemDO::getRelatedRequirementId, null) + .eq(PmsWorkItemDO::getRelatedRequirementId, relatedRequirementId)); + } + + default void deleteByProjectId(Long projectId) { + delete(PmsWorkItemDO::getProjectId, projectId); + } + + static LambdaQueryWrapperX buildQueryWrapper(PmsWorkItemPageReqVO pageReqVO) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eq(PmsWorkItemDO::getProjectId, pageReqVO.getProjectId()) + .eq(PmsWorkItemDO::getLifecycleStatus, pageReqVO.getLifecycleStatus() != null + ? pageReqVO.getLifecycleStatus() : PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .eqIfPresent(PmsWorkItemDO::getType, pageReqVO.getType()) + .inIfPresent(PmsWorkItemDO::getType, pageReqVO.getTypes()) + .eqIfPresent(PmsWorkItemDO::getStatus, pageReqVO.getStatus()) + .inIfPresent(PmsWorkItemDO::getStatus, pageReqVO.getStatuses()) + .eqIfPresent(PmsWorkItemDO::getPriority, pageReqVO.getPriority()) + .inIfPresent(PmsWorkItemDO::getPriority, pageReqVO.getPriorities()) + .eqIfPresent(PmsWorkItemDO::getStatusId, pageReqVO.getStatusId()) + .eqIfPresent(PmsWorkItemDO::getIterationId, pageReqVO.getIterationId()) + .inIfPresent(PmsWorkItemDO::getIterationId, pageReqVO.getIterationIds()) + .eqIfPresent(PmsWorkItemDO::getParentId, pageReqVO.getParentId()) + .eqIfPresent(PmsWorkItemDO::getAssigneeUserId, pageReqVO.getAssigneeUserId()) + .inIfPresent(PmsWorkItemDO::getAssigneeUserId, pageReqVO.getAssigneeUserIds()); + appendNameOrSerialNumberQuery(queryWrapper, pageReqVO.getName()); + if (Boolean.TRUE.equals(pageReqVO.getUnplannedOnly())) { + queryWrapper.isNull(PmsWorkItemDO::getIterationId); + } + if (Boolean.TRUE.equals(pageReqVO.getRootOnly())) { + queryWrapper.isNull(PmsWorkItemDO::getParentId); + } + if (CollUtil.isNotEmpty(pageReqVO.getExcludedIterationIds())) { + queryWrapper.and(wrapper -> wrapper.notIn(PmsWorkItemDO::getIterationId, + pageReqVO.getExcludedIterationIds()).or().isNull(PmsWorkItemDO::getIterationId)); + } + // label_ids 为 JSON 数组列,使用 MySQL JSON_CONTAINS 匹配元素,H2 单测不覆盖该分支 + if (CollUtil.isNotEmpty(pageReqVO.getLabelIds())) { + queryWrapper.and(labelWrapper -> { + for (Long labelId : pageReqVO.getLabelIds()) { + labelWrapper.or().apply("JSON_CONTAINS(label_ids, JSON_ARRAY({0}))", labelId); + } + }); + } + return queryWrapper; + } + + /** + * 按工作项名称或编号追加模糊查询条件 + * + * @param queryWrapper 查询条件 + * @param keyword 关键词,可匹配名称或去掉前缀后的编号 + */ + static void appendNameOrSerialNumberQuery(LambdaQueryWrapperX queryWrapper, String keyword) { + if (StrUtil.isBlank(keyword)) { + return; + } + String serialNumberKeyword = StrUtil.removePrefix(keyword, "#"); + queryWrapper.and(wrapper -> { + wrapper.like(PmsWorkItemDO::getName, keyword); + if (StrUtil.isNotBlank(serialNumberKeyword)) { + wrapper.or().like(PmsWorkItemDO::getSerialNumber, serialNumberKeyword); + } + }); + } + + static void appendNameOrSerialNumberQuery(QueryWrapperX queryWrapper, String keyword) { + if (StrUtil.isBlank(keyword)) { + return; + } + String serialNumberKeyword = StrUtil.removePrefix(keyword, "#"); + queryWrapper.and(wrapper -> { + wrapper.like("name", keyword); + if (StrUtil.isNotBlank(serialNumberKeyword)) { + wrapper.or().like("serial_number", serialNumberKeyword); + } + }); + } + + default void updateIterationIdById(Long id, Long iterationId) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemDO::getIterationId, iterationId) + .eq(PmsWorkItemDO::getId, id)); + } + + default void updateLifecycleStatusById(Long id, Integer lifecycleStatus, LocalDateTime archiveTime, + LocalDateTime recycleTime) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemDO::getLifecycleStatus, lifecycleStatus) + .set(PmsWorkItemDO::getArchiveTime, archiveTime) + .set(PmsWorkItemDO::getRecycleTime, recycleTime) + .eq(PmsWorkItemDO::getId, id)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemMemberMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemMemberMapper.java new file mode 100644 index 000000000..d2872a09c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemMemberMapper.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemMemberDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsWorkItemMemberMapper extends BaseMapperX { + + default List selectListByWorkItemIds(Collection workItemIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsWorkItemMemberDO::getWorkItemId, workItemIds) + .orderByAsc(PmsWorkItemMemberDO::getId)); + } + + default void deleteByWorkItemId(Long workItemId) { + delete(PmsWorkItemMemberDO::getWorkItemId, workItemId); + } + + default void deleteByProjectId(Long projectId) { + delete(PmsWorkItemMemberDO::getProjectId, projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemStatusMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemStatusMapper.java new file mode 100644 index 000000000..b0d9702cd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemStatusMapper.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsWorkItemStatusMapper extends BaseMapperX { + + default List selectListByProjectIdAndWorkItemType(Long projectId, Integer workItemType) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemStatusDO::getProjectId, projectId) + .eq(PmsWorkItemStatusDO::getWorkItemType, workItemType) + .orderByAsc(PmsWorkItemStatusDO::getSort) + .orderByAsc(PmsWorkItemStatusDO::getId)); + } + + default PmsWorkItemStatusDO selectDefaultByProjectIdAndWorkItemType(Long projectId, Integer workItemType) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsWorkItemStatusDO::getProjectId, projectId) + .eq(PmsWorkItemStatusDO::getWorkItemType, workItemType) + .eq(PmsWorkItemStatusDO::getDefaultStatus, true)); + } + + default PmsWorkItemStatusDO selectByProjectIdAndWorkItemTypeAndName(Long projectId, Integer workItemType, + String name) { + return selectOne(new LambdaQueryWrapperX() + .eq(PmsWorkItemStatusDO::getProjectId, projectId) + .eq(PmsWorkItemStatusDO::getWorkItemType, workItemType) + .eq(PmsWorkItemStatusDO::getName, name)); + } + + default void updateDefaultStatusByProjectIdAndWorkItemType(Long projectId, Integer workItemType, + Long defaultStatusId) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemStatusDO::getDefaultStatus, false) + .eq(PmsWorkItemStatusDO::getProjectId, projectId) + .eq(PmsWorkItemStatusDO::getWorkItemType, workItemType)); + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemStatusDO::getDefaultStatus, true) + .eq(PmsWorkItemStatusDO::getId, defaultStatusId) + .eq(PmsWorkItemStatusDO::getProjectId, projectId) + .eq(PmsWorkItemStatusDO::getWorkItemType, workItemType)); + } + + default void deleteByProjectId(Long projectId) { + delete(PmsWorkItemStatusDO::getProjectId, projectId); + } + + default void updateBoardNameByProjectIdAndWorkItemType(Long projectId, Integer workItemType, String boardName) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemStatusDO::getBoardName, boardName) + .eq(PmsWorkItemStatusDO::getProjectId, projectId) + .eq(PmsWorkItemStatusDO::getWorkItemType, workItemType)); + } + + default void updateBoardNameByIds(Collection ids, String boardName) { + update(null, new LambdaUpdateWrapper() + .set(PmsWorkItemStatusDO::getBoardName, boardName) + .in(PmsWorkItemStatusDO::getId, ids)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemUserSortMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemUserSortMapper.java new file mode 100644 index 000000000..21c97997e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemUserSortMapper.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PmsWorkItemUserSortMapper extends BaseMapperX { + + default List selectListByProjectIdAndUserId(Long projectId, Long userId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemUserSortDO::getProjectId, projectId) + .eq(PmsWorkItemUserSortDO::getUserId, userId) + .orderByAsc(PmsWorkItemUserSortDO::getSort) + .orderByAsc(PmsWorkItemUserSortDO::getId)); + } + + default void deleteByProjectIdAndUserId(Long projectId, Long userId) { + delete(new LambdaQueryWrapperX() + .eq(PmsWorkItemUserSortDO::getProjectId, projectId) + .eq(PmsWorkItemUserSortDO::getUserId, userId)); + } + + default void deleteByWorkItemId(Long workItemId) { + delete(new LambdaQueryWrapperX() + .eq(PmsWorkItemUserSortDO::getWorkItemId, workItemId)); + } + + default void deleteByProjectId(Long projectId) { + delete(new LambdaQueryWrapperX() + .eq(PmsWorkItemUserSortDO::getProjectId, projectId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemWorkLogMapper.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemWorkLogMapper.java new file mode 100644 index 000000000..7b84a5dcd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemWorkLogMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PmsWorkItemWorkLogMapper extends BaseMapperX { + + default List selectListByWorkItemId(Long workItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemWorkLogDO::getWorkItemId, workItemId) + .orderByDesc(PmsWorkItemWorkLogDO::getCreateTime) + .orderByDesc(PmsWorkItemWorkLogDO::getId)); + } + + default List selectListByWorkItemIds(Collection workItemIds) { + return selectList(new LambdaQueryWrapperX() + .in(PmsWorkItemWorkLogDO::getWorkItemId, workItemIds) + .orderByAsc(PmsWorkItemWorkLogDO::getCreateTime) + .orderByAsc(PmsWorkItemWorkLogDO::getId)); + } + + default List selectListByProjectIdAndCreateTimeBetween( + Long projectId, LocalDateTime beginTime, LocalDateTime endTime) { + return selectList(new LambdaQueryWrapperX() + .eq(PmsWorkItemWorkLogDO::getProjectId, projectId) + .between(PmsWorkItemWorkLogDO::getCreateTime, beginTime, endTime) + .orderByAsc(PmsWorkItemWorkLogDO::getCreateTime) + .orderByAsc(PmsWorkItemWorkLogDO::getId)); + } + + default int updateForEdit(PmsWorkItemWorkLogDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .set(updateObj.getDescription() == null, PmsWorkItemWorkLogDO::getDescription, null) + .eq(PmsWorkItemWorkLogDO::getId, updateObj.getId())); + } + + default void deleteByProjectId(Long projectId) { + delete(PmsWorkItemWorkLogDO::getProjectId, projectId); + } + + default void deleteByWorkItemId(Long workItemId) { + delete(PmsWorkItemWorkLogDO::getWorkItemId, workItemId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/package-info.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/package-info.java new file mode 100644 index 000000000..855cabee6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/package-info.java @@ -0,0 +1,4 @@ +/** + * PMS 模块的 Framework 封装 + */ +package cn.iocoder.yudao.module.pms.framework; diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/rpc/config/RpcConfiguration.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/rpc/config/RpcConfiguration.java new file mode 100644 index 000000000..b1c7ddd27 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/rpc/config/RpcConfiguration.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.pms.framework.rpc.config; + +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration(value = "pmsRpcConfiguration", proxyBeanMethods = false) +@EnableFeignClients(clients = {AdminUserApi.class, DeptApi.class, PermissionApi.class, + NotifyMessageSendApi.class, FileApi.class}) +public class RpcConfiguration { +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/security/config/SecurityConfiguration.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/security/config/SecurityConfiguration.java new file mode 100644 index 000000000..f0f849e3f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.pms.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import cn.iocoder.yudao.module.pms.enums.ApiConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * PMS 模块的 Security 配置 + */ +@Configuration("pmsSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("pmsAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // Swagger 接口文档 + registry.requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers("/webjars/**").permitAll() + .requestMatchers("/swagger-ui").permitAll() + .requestMatchers("/swagger-ui/**").permitAll(); + // Spring Boot Actuator 的安全配置 + registry.requestMatchers("/actuator").permitAll() + .requestMatchers("/actuator/**").permitAll(); + // Druid 监控 + registry.requestMatchers("/druid/**").permitAll(); + // RPC 服务的安全配置 + registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); + } + + }; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/web/config/PmsWebConfiguration.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/web/config/PmsWebConfiguration.java new file mode 100644 index 000000000..288746dfe --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/web/config/PmsWebConfiguration.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.pms.framework.web.config; + +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * PMS 模块的 Web 配置 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class PmsWebConfiguration { + + /** + * 创建 PMS 模块的 OpenAPI 分组 + * + * @return PMS OpenAPI 分组 + */ + @Bean + public GroupedOpenApi pmsGroupedOpenApi() { + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("pms"); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/web/package-info.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/web/package-info.java new file mode 100644 index 000000000..f7e65e9f1 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * PMS 模块的 Web 配置 + */ +package cn.iocoder.yudao.module.pms.framework.web; diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/job/kb/PmsKnowledgeRecycleCleanJob.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/job/kb/PmsKnowledgeRecycleCleanJob.java new file mode 100644 index 000000000..0d270e412 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/job/kb/PmsKnowledgeRecycleCleanJob.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.pms.job.kb; + +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.pms.service.kb.recycle.PmsKnowledgeRecycleService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * PMS 知识库回收站到期清理 Job + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class PmsKnowledgeRecycleCleanJob { + + private static final int RETAIN_DAYS = 30; + + @Resource + private PmsKnowledgeRecycleService recycleService; + + @XxlJob("pmsKnowledgeRecycleCleanJob") + @TenantJob + public String execute() { + LocalDateTime deleteTime = LocalDateTime.now().minusDays(RETAIN_DAYS); + int cleanCount = recycleService.deleteExpiredRecycleRecords(deleteTime); + log.info("[execute][清理知识库到期回收站记录 ({}) 条]", cleanCount); + return String.format("清理知识库到期回收站记录 %s 条", cleanCount); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/job/package-info.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/job/package-info.java new file mode 100644 index 000000000..30d81b4c5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/job/package-info.java @@ -0,0 +1,4 @@ +/** + * PMS 模块的定时任务 + */ +package cn.iocoder.yudao.module.pms.job; diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/package-info.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/package-info.java new file mode 100644 index 000000000..43fc4323b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/package-info.java @@ -0,0 +1,10 @@ +/** + * pms 包下,项目管理(Project Management System) + * 例如说:项目、工作事项、迭代、看板、工时和知识库等等 + * + * 1. Controller URL:以 /pms/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 pms_ 开头,方便在数据库中区分 + * + * 注意,由于 PMS 模块下容易和其它模块重名,所以类名都加 Pms 前缀 + */ +package cn.iocoder.yudao.module.pms; diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionService.java new file mode 100644 index 000000000..599444aef --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionService.java @@ -0,0 +1,165 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.permission.PmsKnowledgeContentPermissionUpdateReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionMemberDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * PMS 知识内容协作权限 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeContentPermissionService { + + /** + * 创建默认内容权限,公开仅可预览,创建人固定拥有管理权限 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 内容权限编号 + */ + Long createDefaultContentPermission(Long libraryId, Long userId); + + /** + * 复制内容权限及其协作者到目标知识库,同一原权限对应同一新权限 + * + * @param permissionIds 内容权限编号集合 + * @param targetLibraryId 目标知识库编号 + * @return 原权限编号到新权限编号的映射 + */ + Map cloneContentPermissions(Collection permissionIds, Long targetLibraryId); + + /** + * 校验当前用户对内容有读取权限 + * + * @param permissionId 内容权限编号 + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 内容等级 + */ + Integer validateContentPermissionReadable(Long permissionId, Long libraryId, Long userId); + + /** + * 校验当前用户对内容有编辑权限 + * + * @param permissionId 内容权限编号 + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 内容等级 + */ + Integer validateContentPermissionWritable(Long permissionId, Long libraryId, Long userId); + + /** + * 校验当前用户对内容有删除权限 + * + * @param permissionId 内容权限编号 + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 内容等级 + */ + Integer validateContentPermissionDeletable(Long permissionId, Long libraryId, Long userId); + + /** + * 校验当前用户对内容有管理权限 + * + * @param permissionId 内容权限编号 + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 内容等级 + */ + Integer validateContentPermissionManageable(Long permissionId, Long libraryId, Long userId); + + /** + * 判断当前用户对内容是否有读取权限 + * + * @param permissionId 内容权限编号 + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 是否可读 + */ + boolean isContentReadable(Long permissionId, Long libraryId, Long userId); + + /** + * 获得内容权限,校验当前用户有读取权限 + * + * @param permissionId 内容权限编号 + * @param userId 用户编号 + * @return 内容权限 + */ + PmsKnowledgeContentPermissionDO getContentPermission(Long permissionId, Long userId); + + /** + * 获得内容权限的协作者列表,校验当前用户有读取权限 + * + * @param permissionId 内容权限编号 + * @param userId 用户编号 + * @return 协作者列表 + */ + List getContentPermissionMemberList(Long permissionId, Long userId); + + /** + * 获得当前用户对内容的协作等级,无权限时返回 {@code null} + * + * @param permissionId 内容权限编号 + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 内容等级 + */ + Integer getCurrentUserContentPermissionLevel(Long permissionId, Long libraryId, Long userId); + + /** + * 获得当前用户对一批内容的协作等级 Map,无权限的权限编号对应 {@code null} + * + * @param permissionIds 内容权限编号集合 + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 内容权限编号到等级的映射 + */ + Map getCurrentUserContentPermissionLevelMap(Collection permissionIds, Long libraryId, Long userId); + + /** + * 获得当前用户在知识库中可读的内容权限编号集合 + * + * @param libraryIds 知识库编号集合 + * @param userId 用户编号 + * @return 可读内容权限编号集合 + */ + Set getReadableContentPermissionIdSet(Collection libraryIds, Long userId); + + /** + * 更新内容权限的公开设置和协作者,保留创建人的管理权限 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateContentPermission(PmsKnowledgeContentPermissionUpdateReqVO updateReqVO, Long userId); + + /** + * 删除不再被任何文件夹或文档引用的内容权限 + * + * @param permissionIds 内容权限编号集合 + */ + void deleteUnusedContentPermissions(Set permissionIds); + + /** + * 删除知识库的全部内容权限 + * + * @param libraryId 知识库编号 + */ + void deleteContentPermissionsByLibraryId(Long libraryId); + + /** + * 删除知识库内容权限中指定用户和部门的协作者 + * + * @param libraryId 知识库编号 + * @param userIds 用户编号集合 + * @param deptIds 部门编号集合 + */ + void deleteContentPermissionMembersByLibraryId(Long libraryId, Collection userIds, Collection deptIds); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionServiceImpl.java new file mode 100644 index 000000000..f18dcb580 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionServiceImpl.java @@ -0,0 +1,417 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.permission.PmsKnowledgeContentPermissionUpdateReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionMemberDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeContentPermissionMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeContentPermissionMemberMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMemberMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeContentLevelEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.equalsAny; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_CONTENT_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_CONTENT_DELETE_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_CONTENT_PERMISSION_MEMBERS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_CONTENT_PERMISSION_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_CONTENT_WRITE_ACCESS_DENIED; + +/** + * PMS 知识内容协作权限 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeContentPermissionServiceImpl implements PmsKnowledgeContentPermissionService { + + @Resource + private PmsKnowledgeContentPermissionMapper permissionMapper; + @Resource + private PmsKnowledgeContentPermissionMemberMapper permissionMemberMapper; + + @Resource + @Lazy + private PmsKnowledgeFolderService folderService; + @Resource + @Lazy + private PmsKnowledgeDocumentService documentService; + @Resource + private PmsKnowledgeLibraryMemberMapper libraryMemberMapper; + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + @Resource + private PermissionApi permissionApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createDefaultContentPermission(Long libraryId, Long userId) { + // 1. 创建默认公开且仅预览的内容权限 + PmsKnowledgeContentPermissionDO permission = new PmsKnowledgeContentPermissionDO().setLibraryId(libraryId) + .setOpenStatus(true).setOpenLevel(PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permission.setCreator(String.valueOf(userId)); + permissionMapper.insert(permission); + + // 2. 创建人固定拥有管理权限 + permissionMemberMapper.insert(new PmsKnowledgeContentPermissionMemberDO().setPermissionId(permission.getId()).setUserId(userId) + .setLevel(PmsKnowledgeContentLevelEnum.MANAGE.getLevel())); + return permission.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map cloneContentPermissions(Collection permissionIds, Long targetLibraryId) { + if (CollUtil.isEmpty(permissionIds)) { + return Collections.emptyMap(); + } + // 1. 批量查询原权限及协作者 + List permissions = permissionMapper.selectByIds(permissionIds); + if (permissions.size() != permissionIds.size()) { + throw exception(KNOWLEDGE_CONTENT_PERMISSION_NOT_EXISTS); + } + List members = permissionMemberMapper.selectListByPermissionIds(permissionIds); + + // 2. 逐个复制权限和协作者,保持同一原权限仍对应同一新权限 + Map clonedPermissionIdMap = new LinkedHashMap<>(); + for (PmsKnowledgeContentPermissionDO source : permissions) { + PmsKnowledgeContentPermissionDO target = new PmsKnowledgeContentPermissionDO().setLibraryId(targetLibraryId) + .setOpenStatus(source.getOpenStatus()).setOpenLevel(source.getOpenLevel()); + target.setCreator(source.getCreator()); + permissionMapper.insert(target); + List targetMembers = convertList(members, member -> + new PmsKnowledgeContentPermissionMemberDO().setPermissionId(target.getId()) + .setUserId(member.getUserId()).setDeptId(member.getDeptId()).setLevel(member.getLevel()), + member -> source.getId().equals(member.getPermissionId())); + if (CollUtil.isNotEmpty(targetMembers)) { + permissionMemberMapper.insertBatch(targetMembers); + } + clonedPermissionIdMap.put(source.getId(), target.getId()); + } + return clonedPermissionIdMap; + } + + @Override + public Integer validateContentPermissionReadable(Long permissionId, Long libraryId, Long userId) { + Integer level = getCurrentUserContentPermissionLevel(permissionId, libraryId, userId); + if (level == null) { + throw exception(KNOWLEDGE_CONTENT_ACCESS_DENIED); + } + return level; + } + + @Override + public Integer validateContentPermissionWritable(Long permissionId, Long libraryId, Long userId) { + Integer level = validateContentPermissionReadable(permissionId, libraryId, userId); + if (!PmsKnowledgeContentLevelEnum.canEdit(level)) { + throw exception(KNOWLEDGE_CONTENT_WRITE_ACCESS_DENIED); + } + return level; + } + + @Override + public Integer validateContentPermissionDeletable(Long permissionId, Long libraryId, Long userId) { + Integer level = validateContentPermissionReadable(permissionId, libraryId, userId); + if (!PmsKnowledgeContentLevelEnum.canDelete(level)) { + throw exception(KNOWLEDGE_CONTENT_DELETE_ACCESS_DENIED); + } + return level; + } + + @Override + public Integer validateContentPermissionManageable(Long permissionId, Long libraryId, Long userId) { + Integer level = validateContentPermissionReadable(permissionId, libraryId, userId); + if (ObjectUtil.notEqual(PmsKnowledgeContentLevelEnum.MANAGE.getLevel(), level)) { + throw exception(KNOWLEDGE_CONTENT_WRITE_ACCESS_DENIED); + } + return level; + } + + @Override + public boolean isContentReadable(Long permissionId, Long libraryId, Long userId) { + return getCurrentUserContentPermissionLevel(permissionId, libraryId, userId) != null; + } + + @Override + public PmsKnowledgeContentPermissionDO getContentPermission(Long permissionId, Long userId) { + // 1. 校验内容权限存在 + PmsKnowledgeContentPermissionDO permission = validatePermissionExists(permissionId); + // 2. 校验当前用户可以读取知识库 + libraryMemberService.validateLibraryReadable(permission.getLibraryId(), userId); + // 3. 校验当前用户可以读取该内容 + validateContentPermissionReadable(permissionId, permission.getLibraryId(), userId); + return permission; + } + + @Override + public List getContentPermissionMemberList(Long permissionId, Long userId) { + // 1. 校验内容权限存在、知识库可读且内容可读 + PmsKnowledgeContentPermissionDO permission = validatePermissionExists(permissionId); + libraryMemberService.validateLibraryReadable(permission.getLibraryId(), userId); + validateContentPermissionReadable(permissionId, permission.getLibraryId(), userId); + + // 2. 返回协作者列表 + return permissionMemberMapper.selectListByPermissionId(permissionId); + } + + @Override + public Integer getCurrentUserContentPermissionLevel(Long permissionId, Long libraryId, Long userId) { + return getCurrentUserContentPermissionLevelMap(Collections.singleton(permissionId), libraryId, userId).get(permissionId); + } + + @Override + public Map getCurrentUserContentPermissionLevelMap(Collection permissionIds, Long libraryId, Long userId) { + if (CollUtil.isEmpty(permissionIds)) { + return Collections.emptyMap(); + } + // 1. 批量查询内容权限并校验知识库归属 + List permissions = permissionMapper.selectByIds(permissionIds); + if (permissions.size() != permissionIds.size() + || permissions.stream().anyMatch(permission -> ObjectUtil.notEqual(libraryId, permission.getLibraryId()))) { + throw exception(KNOWLEDGE_CONTENT_PERMISSION_NOT_EXISTS); + } + Map levelMap = new LinkedHashMap<>(); + if (libraryMemberService.isLibraryAdmin(libraryId, userId)) { + permissions.forEach(permission -> levelMap.put( + permission.getId(), PmsKnowledgeContentLevelEnum.MANAGE.getLevel())); + return levelMap; + } + + // 2. 公开权限优先;私有内容再按用户、部门顺序匹配显式权限 + AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); + Long deptId = user != null ? user.getDeptId() : null; + Map userMemberMap = new LinkedHashMap<>(); + Map deptMemberMap = new LinkedHashMap<>(); + for (PmsKnowledgeContentPermissionMemberDO member : permissionMemberMapper.selectListByPermissionIds(permissionIds)) { + if (userId.equals(member.getUserId())) { + userMemberMap.put(member.getPermissionId(), member); + } else if (deptId != null && deptId.equals(member.getDeptId())) { + deptMemberMap.put(member.getPermissionId(), member); + } + } + for (PmsKnowledgeContentPermissionDO permission : permissions) { + PmsKnowledgeContentPermissionMemberDO userMember = userMemberMap.get(permission.getId()); + PmsKnowledgeContentPermissionMemberDO deptMember = deptMemberMap.get(permission.getId()); + Integer level = Boolean.TRUE.equals(permission.getOpenStatus()) ? permission.getOpenLevel() + : userMember != null ? userMember.getLevel() + : deptMember != null ? deptMember.getLevel() + : null; + levelMap.put(permission.getId(), level); + } + return levelMap; + } + + @Override + public Set getReadableContentPermissionIdSet(Collection libraryIds, Long userId) { + if (CollUtil.isEmpty(libraryIds)) { + return Collections.emptySet(); + } + // 1.1 一次查询全部知识库的内容权限 + List permissions = permissionMapper.selectListByLibraryIds(libraryIds); + if (CollUtil.isEmpty(permissions)) { + return Collections.emptySet(); + } + Set permissionIds = convertSet(permissions, PmsKnowledgeContentPermissionDO::getId); + // 1.2 超级管理员可以读取全部内容 + if (permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData()) { + return permissionIds; + } + + // 2.1 一次查询当前用户及其部门的知识库成员身份,用户身份优先于部门身份 + AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); + Long deptId = user != null ? user.getDeptId() : null; + Map userLibraryMemberMap = new LinkedHashMap<>(); + Map deptLibraryMemberMap = new LinkedHashMap<>(); + List libraryMembers = libraryMemberMapper.selectListByLibraryIds(libraryIds); + for (PmsKnowledgeLibraryMemberDO member : libraryMembers) { + if (userId.equals(member.getUserId())) { + userLibraryMemberMap.putIfAbsent(member.getLibraryId(), member); + } else if (deptId != null && deptId.equals(member.getDeptId())) { + deptLibraryMemberMap.putIfAbsent(member.getLibraryId(), member); + } + } + Set adminLibraryIds = new LinkedHashSet<>(); + for (Long libraryId : libraryIds) { + PmsKnowledgeLibraryMemberDO member = userLibraryMemberMap.get(libraryId); + if (member == null) { + member = deptLibraryMemberMap.get(libraryId); + } + if (member != null && equalsAny(member.getLevel(), + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel(), + PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel())) { + adminLibraryIds.add(libraryId); + } + } + // 2.2 一次查询当前用户及其部门的内容协作权限,用户权限优先于部门权限 + Map userPermissionMemberMap = new LinkedHashMap<>(); + Map deptPermissionMemberMap = new LinkedHashMap<>(); + for (PmsKnowledgeContentPermissionMemberDO member : permissionMemberMapper.selectListByPermissionIds(permissionIds)) { + if (userId.equals(member.getUserId())) { + userPermissionMemberMap.put(member.getPermissionId(), member); + } else if (deptId != null && deptId.equals(member.getDeptId())) { + deptPermissionMemberMap.put(member.getPermissionId(), member); + } + } + + // 3. 库管理员、本人和部门的显式权限优先于公开权限 + Set readablePermissionIds = new LinkedHashSet<>(); + for (PmsKnowledgeContentPermissionDO permission : permissions) { + if (adminLibraryIds.contains(permission.getLibraryId()) + || userPermissionMemberMap.containsKey(permission.getId()) + || deptPermissionMemberMap.containsKey(permission.getId()) + || Boolean.TRUE.equals(permission.getOpenStatus())) { + readablePermissionIds.add(permission.getId()); + } + } + return readablePermissionIds; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateContentPermission(PmsKnowledgeContentPermissionUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验内容权限存在 + PmsKnowledgeContentPermissionDO permission = validatePermissionExists(updateReqVO.getId()); + // 1.2 校验知识库可访问 + PmsKnowledgeLibraryDO library = libraryMemberService.validateLibraryReadable(permission.getLibraryId(), userId); + // 1.3 校验内容管理权限 + if (!PmsKnowledgeContentLevelEnum.MANAGE.getLevel().equals( + getCurrentUserContentPermissionLevel(permission.getId(), permission.getLibraryId(), userId))) { + throw exception(KNOWLEDGE_CONTENT_WRITE_ACCESS_DENIED); + } + // 1.4 校验协作者唯一 + Set userIds = convertSet(updateReqVO.getMembers(), + PmsKnowledgeContentPermissionUpdateReqVO.Member::getUserId); + Set deptIds = convertSet(updateReqVO.getMembers(), + PmsKnowledgeContentPermissionUpdateReqVO.Member::getDeptId); + if (userIds.size() + deptIds.size() != updateReqVO.getMembers().size()) { + throw exception(KNOWLEDGE_CONTENT_PERMISSION_MEMBERS_INVALID); + } + // 1.5 校验协作者不能包含内容创建人 + if (userIds.contains(NumberUtils.parseLong(permission.getCreator()))) { + throw exception(KNOWLEDGE_CONTENT_PERMISSION_MEMBERS_INVALID); + } + // 1.6 校验用户协作者存在 + adminUserApi.validateUserList(userIds).checkError(); + // 1.7 校验部门协作者存在 + deptApi.validateDeptList(deptIds).checkError(); + // 1.8 私有知识库的用户协作者必须是知识库成员 + if (!library.getOpenStatus()) { + List libraryMembers = libraryMemberMapper.selectListByLibraryId( + library.getId()); + Set libraryUserIds = convertSet(libraryMembers, PmsKnowledgeLibraryMemberDO::getUserId); + Set libraryDeptIds = convertSet(libraryMembers, PmsKnowledgeLibraryMemberDO::getDeptId); + if (!libraryUserIds.containsAll(userIds)) { + throw exception(KNOWLEDGE_CONTENT_PERMISSION_MEMBERS_INVALID); + } + // 1.9 私有知识库的部门协作者必须是知识库成员 + if (!libraryDeptIds.containsAll(deptIds)) { + throw exception(KNOWLEDGE_CONTENT_PERMISSION_MEMBERS_INVALID); + } + } + + // 2. 更新公开权限并重建协作者 + permissionMapper.updateById(BeanUtils.toBean(updateReqVO, PmsKnowledgeContentPermissionDO.class) + .setId(permission.getId()).setLibraryId(null)); + permissionMemberMapper.deleteByPermissionId(permission.getId()); + List members = convertList(updateReqVO.getMembers(), member -> + new PmsKnowledgeContentPermissionMemberDO().setPermissionId(permission.getId()).setUserId(member.getUserId()) + .setDeptId(member.getDeptId()).setLevel(member.getLevel())); + members.add(0, new PmsKnowledgeContentPermissionMemberDO().setPermissionId(permission.getId()) + .setUserId(NumberUtils.parseLong(permission.getCreator())) + .setLevel(PmsKnowledgeContentLevelEnum.MANAGE.getLevel())); + permissionMemberMapper.insertBatch(members); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteUnusedContentPermissions(Set permissionIds) { + if (CollUtil.isEmpty(permissionIds)) { + return; + } + // 仅删除已经不被任何文件夹或文档引用的权限,避免同一权限被兄弟内容复用时误删 + permissionIds.removeAll(folderService.getExistingContentPermissionIdSet(permissionIds)); + permissionIds.removeAll(documentService.getExistingContentPermissionIdSet(permissionIds)); + if (CollUtil.isEmpty(permissionIds)) { + return; + } + + permissionMemberMapper.deleteByPermissionIds(permissionIds); + permissionMapper.deleteByIds(permissionIds); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteContentPermissionsByLibraryId(Long libraryId) { + List permissions = permissionMapper.selectListByLibraryId(libraryId); + Set permissionIds = convertSet(permissions, PmsKnowledgeContentPermissionDO::getId); + if (CollUtil.isEmpty(permissionIds)) { + return; + } + + // 先删除权限成员关系,再删除权限主记录 + permissionMemberMapper.deleteByPermissionIds(permissionIds); + permissionMapper.deleteByIds(permissionIds); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteContentPermissionMembersByLibraryId(Long libraryId, Collection userIds, Collection deptIds) { + if (CollUtil.isEmpty(userIds) && CollUtil.isEmpty(deptIds)) { + return; + } + Set permissionIds = convertSet(permissionMapper.selectListByLibraryId(libraryId), + PmsKnowledgeContentPermissionDO::getId); + if (CollUtil.isEmpty(permissionIds)) { + return; + } + + // 分别清理用户和部门的内容级协作关系 + if (CollUtil.isNotEmpty(userIds)) { + permissionMemberMapper.deleteByPermissionIdsAndUserIds(permissionIds, userIds); + } + if (CollUtil.isNotEmpty(deptIds)) { + permissionMemberMapper.deleteByPermissionIdsAndDeptIds(permissionIds, deptIds); + } + } + + private PmsKnowledgeContentPermissionDO validatePermissionExists(Long permissionId) { + PmsKnowledgeContentPermissionDO permission = permissionMapper.selectById(permissionId); + if (permission == null) { + throw exception(KNOWLEDGE_CONTENT_PERMISSION_NOT_EXISTS); + } + return permission; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelService.java new file mode 100644 index 000000000..3f7e9e67b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelService.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentLabelDO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 知识库文档标签 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeDocumentLabelService { + + /** + * 创建文档标签 + * + * @param saveReqVO 保存信息 + * @return 标签编号 + */ + Long createDocumentLabel(PmsKnowledgeDocumentLabelSaveReqVO saveReqVO); + + /** + * 更新文档标签 + * + * @param saveReqVO 保存信息 + */ + void updateDocumentLabel(PmsKnowledgeDocumentLabelSaveReqVO saveReqVO); + + /** + * 删除文档标签 + * + * @param id 标签编号 + */ + void deleteDocumentLabel(Long id); + + /** + * 获得文档标签 + * + * @param id 标签编号 + * @return 文档标签 + */ + PmsKnowledgeDocumentLabelDO getDocumentLabel(Long id); + + /** + * 获得文档标签列表 + * + * @return 标签列表 + */ + List getDocumentLabelList(); + + /** + * 校验文档标签均存在 + * + * @param ids 标签编号集合 + */ + void validateDocumentLabelList(Collection ids); + + /** + * 按标签获得当前用户可读的文档分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 文档分页 + */ + PageResult getDocumentPageByLabel( + PmsKnowledgeDocumentLabelPageReqVO pageReqVO, Long userId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelServiceImpl.java new file mode 100644 index 000000000..4a91d81fd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelServiceImpl.java @@ -0,0 +1,125 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentLabelDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentLabelMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_LABEL_NOT_EXISTS; + +/** + * PMS 知识库文档标签 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeDocumentLabelServiceImpl implements PmsKnowledgeDocumentLabelService { + + @Resource + private PmsKnowledgeDocumentLabelMapper documentLabelMapper; + @Resource + private PmsKnowledgeDocumentMapper documentMapper; + + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + + @Override + public Long createDocumentLabel(PmsKnowledgeDocumentLabelSaveReqVO saveReqVO) { + PmsKnowledgeDocumentLabelDO documentLabel = BeanUtils.toBean(saveReqVO, PmsKnowledgeDocumentLabelDO.class) + .setName(StrUtil.trim(saveReqVO.getName())).setColor(StrUtil.trim(saveReqVO.getColor())); + documentLabelMapper.insert(documentLabel); + return documentLabel.getId(); + } + + @Override + public void updateDocumentLabel(PmsKnowledgeDocumentLabelSaveReqVO saveReqVO) { + // 1. 校验文档标签存在 + validateDocumentLabelExists(saveReqVO.getId()); + + // 2. 更新文档标签 + documentLabelMapper.updateById(BeanUtils.toBean(saveReqVO, PmsKnowledgeDocumentLabelDO.class) + .setName(StrUtil.trim(saveReqVO.getName())).setColor(StrUtil.trim(saveReqVO.getColor()))); + } + + @Override + public void deleteDocumentLabel(Long id) { + // 1. 校验文档标签存在 + validateDocumentLabelExists(id); + + // 2. 删除文档标签 + documentLabelMapper.deleteById(id); + } + + @Override + public PmsKnowledgeDocumentLabelDO getDocumentLabel(Long id) { + return documentLabelMapper.selectById(id); + } + + @Override + public List getDocumentLabelList() { + return documentLabelMapper.selectList(); + } + + @Override + public void validateDocumentLabelList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + List list = documentLabelMapper.selectByIds(ids); + if (list.size() != ids.size()) { + throw exception(KNOWLEDGE_DOCUMENT_LABEL_NOT_EXISTS); + } + } + + @Override + public PageResult getDocumentPageByLabel( + PmsKnowledgeDocumentLabelPageReqVO pageReqVO, Long userId) { + // 1.1 校验文档标签存在 + validateDocumentLabelExists(pageReqVO.getLabelId()); + // 1.2 获得当前用户可读取的知识库 + List readableLibraryIds = libraryMemberService.getReadableLibraryIdList(userId); + if (CollUtil.isEmpty(readableLibraryIds)) { + return PageResult.empty(); + } + Set readablePermissionIds = contentPermissionService + .getReadableContentPermissionIdSet(readableLibraryIds, userId); + if (CollUtil.isEmpty(readablePermissionIds)) { + return PageResult.empty(); + } + + // 2. 按标签分页查询非回收站文档 + return documentMapper.selectPageByLabelIdAndStatuses( + pageReqVO, readableLibraryIds, readablePermissionIds, + PmsKnowledgeDocumentStatusEnum.ACTIVE_STATUSES); + } + + private PmsKnowledgeDocumentLabelDO validateDocumentLabelExists(Long id) { + PmsKnowledgeDocumentLabelDO documentLabel = documentLabelMapper.selectById(id); + if (documentLabel == null) { + throw exception(KNOWLEDGE_DOCUMENT_LABEL_NOT_EXISTS); + } + return documentLabel; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentService.java new file mode 100644 index 000000000..4be9ba15a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentService.java @@ -0,0 +1,174 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentSearchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * PMS 知识库文档 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeDocumentService { + + /** + * 创建文档 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 文档编号 + */ + Long createDocument(PmsKnowledgeDocumentCreateReqVO createReqVO, Long userId); + + /** + * 更新文档内容 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateDocument(PmsKnowledgeDocumentUpdateReqVO updateReqVO, Long userId); + + /** + * 删除文档,并将文档和全部子文档移入回收站 + * + * @param id 文档编号 + * @param userId 用户编号 + */ + void deleteDocument(Long id, Long userId); + + /** + * 移动文档到目标目录,跨知识库移动时复制内容权限 + * + * @param moveReqVO 移动信息 + * @param userId 用户编号 + */ + void moveDocument(PmsKnowledgeDocumentMoveReqVO moveReqVO, Long userId); + + /** + * 获得文档,校验当前用户有内容读取权限 + * + * @param id 文档编号 + * @param userId 用户编号 + * @return 文档 + */ + PmsKnowledgeDocumentDO getDocument(Long id, Long userId); + + /** + * 获得文档 + * + * @param id 文档编号 + * @return 文档 + */ + PmsKnowledgeDocumentDO getDocument(Long id); + + /** + * 获得文档列表 + * + * @param ids 文档编号集合 + * @return 文档列表 + */ + List getDocumentList(Collection ids); + + /** + * 获得文档 Map + * + * @param ids 文档编号集合 + * @return 文档编号到文档的 Map + */ + default Map getDocumentMap(Collection ids) { + return convertMap(getDocumentList(ids), PmsKnowledgeDocumentDO::getId); + } + + /** + * 获得知识库的全部文档列表 + * + * @param libraryId 知识库编号 + * @return 文档列表 + */ + List getDocumentListByLibraryId(Long libraryId); + + /** + * 批量更新文档 + * + * @param documents 文档列表 + */ + void updateDocumentList(Collection documents); + + /** + * 批量恢复文档 + * + * @param ids 文档编号集合 + */ + void restoreDocumentList(Collection ids); + + /** + * 批量彻底删除文档 + * + * @param ids 文档编号集合 + */ + void deleteDocumentList(Collection ids); + + /** + * 获得文件夹及其子文件夹下的全部文档 + * + * @param folderIds 文件夹编号集合 + * @return 文档及其子文档列表 + */ + List getDocumentListByFolderIds(Collection folderIds); + + /** + * 更新文档移动到其他知识库时的归属和内容权限 + * + * @param documents 文档列表 + * @param targetLibraryId 目标知识库编号 + * @param permissionIdMap 原权限编号到新权限编号的映射 + */ + void moveDocumentList(Collection documents, Long targetLibraryId, + Map permissionIdMap); + + /** + * 使用 MySQL 搜索当前用户可读的文档(用户可按需二次开发,使用 Elasticsearch 检索) + * + * @param pageReqVO 分页搜索条件 + * @param userId 用户编号 + * @return 文档分页 + */ + PageResult getDocumentSearchPage( + PmsKnowledgeDocumentSearchPageReqVO pageReqVO, Long userId); + + /** + * 获得知识库的正常文档列表,仅返回当前用户有内容读取权限的文档 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 文档列表 + */ + List getDocumentList(Long libraryId, Long userId); + + /** + * 获得知识库文档类型数量 Map + * + * @param libraryIds 知识库编号集合 + * @return 知识库编号到文档类型数量 Map + */ + Map> getDocumentTypeCountMap(Collection libraryIds); + + /** + * 获得仍被文档引用的内容权限编号集合 + * + * @param permissionIds 内容权限编号集合 + * @return 被引用的内容权限编号集合 + */ + Set getExistingContentPermissionIdSet(Collection permissionIds); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentServiceImpl.java new file mode 100644 index 000000000..c1e4f6890 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentServiceImpl.java @@ -0,0 +1,462 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentSearchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.recycle.PmsKnowledgeRecycleService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.hutool.core.util.ObjectUtil.notEqual; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_FOLDER_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_MOVE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_MOVE_TARGET_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_PARENT_INVALID; + +/** + * PMS 知识库文档 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeDocumentServiceImpl implements PmsKnowledgeDocumentService { + + @Resource + private PmsKnowledgeDocumentMapper documentMapper; + + @Resource + private PmsKnowledgeFolderService folderService; + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + @Resource + private PmsKnowledgeDocumentLabelService documentLabelService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeFavoriteService favoriteService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeViewRecordService viewRecordService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeRecycleService recycleService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createDocument(PmsKnowledgeDocumentCreateReqVO createReqVO, Long userId) { + // 1.1 校验知识库读取权限 + libraryMemberService.validateLibraryReadable(createReqVO.getLibraryId(), userId); + // 1.2 规范并校验目录位置,同时确定文档权限 + PmsKnowledgeDocumentDO document = BeanUtils.toBean(createReqVO, PmsKnowledgeDocumentDO.class); + Long permissionId = normalizeAndValidateDocumentPlacement(document, null, userId); + if (permissionId == null) { + permissionId = contentPermissionService.createDefaultContentPermission(createReqVO.getLibraryId(), userId); + } + + // 2. 创建文档 + document.setPermissionId(permissionId).setType(createReqVO.getType()) + .setStatus(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + documentMapper.insert(document); + return document.getId(); + } + + @Override + public void updateDocument(PmsKnowledgeDocumentUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验文档存在 + PmsKnowledgeDocumentDO document = validateDocumentExists(updateReqVO.getId()); + // 1.2 校验所属知识库可读 + libraryMemberService.validateLibraryReadable(document.getLibraryId(), userId); + // 1.3 校验内容编辑权限 + contentPermissionService.validateContentPermissionWritable(document.getPermissionId(), document.getLibraryId(), userId); + // 1.4 校验标签均存在 + documentLabelService.validateDocumentLabelList(updateReqVO.getLabelIds()); + + // 2. 更新文档内容 + documentMapper.updateById(BeanUtils.toBean(updateReqVO, PmsKnowledgeDocumentDO.class)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDocument(Long id, Long userId) { + // 1.1 校验文档存在 + PmsKnowledgeDocumentDO document = validateDocumentExists(id); + // 1.2 校验所属知识库可读 + libraryMemberService.validateLibraryReadable(document.getLibraryId(), userId); + // 1.3 校验文档内容删除权限 + contentPermissionService.validateContentPermissionDeletable(document.getPermissionId(), document.getLibraryId(), userId); + + // 2. 将文档和全部子文档移入回收站 + recycleService.recycleDocument(document, getDocumentDescendantList(document), userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void moveDocument(PmsKnowledgeDocumentMoveReqVO moveReqVO, Long userId) { + // 1.1 校验源文档存在 + PmsKnowledgeDocumentDO document = validateDocumentExists(moveReqVO.getId()); + // 1.2 校验移动目标只能选择一种容器 + if (notEqual(PmsKnowledgeDocumentDO.FOLDER_ID_ROOT, moveReqVO.getTargetFolderId()) + && notEqual(PmsKnowledgeDocumentDO.PARENT_ID_ROOT, moveReqVO.getTargetParentId())) { + throw exception(KNOWLEDGE_DOCUMENT_MOVE_TARGET_INVALID); + } + // 1.3 校验源知识库和目标知识库可读 + libraryMemberService.validateLibraryReadable(document.getLibraryId(), userId); + libraryMemberService.validateLibraryReadable(moveReqVO.getTargetLibraryId(), userId); + // 1.4 移动会改变目录结构,校验源文档管理权限 + contentPermissionService.validateContentPermissionManageable( + document.getPermissionId(), document.getLibraryId(), userId); + // 1.5 规范并校验目标位置,禁止移动到自身或子文档 + PmsKnowledgeDocumentDO placement = new PmsKnowledgeDocumentDO() + .setLibraryId(moveReqVO.getTargetLibraryId()).setFolderId(moveReqVO.getTargetFolderId()) + .setParentId(moveReqVO.getTargetParentId()); + normalizeAndValidateDocumentPlacement(placement, document, userId); + + // 2.1 获得文档子树 + List documents = getDocumentDescendantList(document); + // 2.2 跨知识库移动时复制权限,避免影响原知识库内容 + Set sourcePermissionIds = convertSet(documents, PmsKnowledgeDocumentDO::getPermissionId); + boolean crossLibrary = notEqual(document.getLibraryId(), moveReqVO.getTargetLibraryId()); + Map clonedPermissionIdMap = !crossLibrary + ? Collections.emptyMap() + : contentPermissionService.cloneContentPermissions(sourcePermissionIds, moveReqVO.getTargetLibraryId()); + + // 3. 更新文档位置,并同步全部子文档所属知识库与权限 + for (PmsKnowledgeDocumentDO item : documents) { + item.setLibraryId(moveReqVO.getTargetLibraryId()); + if (CollUtil.isNotEmpty(clonedPermissionIdMap)) { + item.setPermissionId(clonedPermissionIdMap.get(item.getPermissionId())); + } + if (item.getId().equals(document.getId())) { + item.setFolderId(placement.getFolderId()).setParentId(placement.getParentId()); + } + } + if (CollUtil.isNotEmpty(documents)) { + documentMapper.updateBatch(documents); + } + + // 4. 跨知识库移动时同步关注和最近浏览记录的当前归属 + if (crossLibrary) { + Set documentIds = convertSet(documents, PmsKnowledgeDocumentDO::getId); + favoriteService.updateFavoriteLibraryIdByEntityIds(Collections.emptySet(), documentIds, moveReqVO.getTargetLibraryId()); + viewRecordService.updateViewRecordLibraryIdByEntityIds(Collections.emptySet(), documentIds, moveReqVO.getTargetLibraryId()); + } + if (CollUtil.isNotEmpty(clonedPermissionIdMap)) { + contentPermissionService.deleteUnusedContentPermissions(sourcePermissionIds); + } + } + + @Override + public PmsKnowledgeDocumentDO getDocument(Long id, Long userId) { + // 1. 校验文档存在 + PmsKnowledgeDocumentDO document = validateDocumentExists(id); + + // 2.1 校验知识库读取权限 + libraryMemberService.validateLibraryReadable(document.getLibraryId(), userId); + // 2.2 校验内容读取权限 + contentPermissionService.validateContentPermissionReadable(document.getPermissionId(), document.getLibraryId(), userId); + return document; + } + + @Override + public PmsKnowledgeDocumentDO getDocument(Long id) { + return documentMapper.selectById(id); + } + + @Override + public List getDocumentList(Collection ids) { + return CollUtil.isEmpty(ids) ? Collections.emptyList() : documentMapper.selectByIds(ids); + } + + @Override + public List getDocumentListByLibraryId(Long libraryId) { + return documentMapper.selectListByLibraryId(libraryId); + } + + @Override + public void updateDocumentList(Collection documents) { + if (CollUtil.isEmpty(documents)) { + return; + } + documentMapper.updateBatch(documents); + } + + @Override + public void restoreDocumentList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + documentMapper.updateToRestoreByIds(ids, PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + } + + @Override + public void deleteDocumentList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + documentMapper.deleteByIds(ids); + } + + @Override + public List getDocumentListByFolderIds(Collection folderIds) { + // 1. 查询文件夹直接包含的根文档 + if (CollUtil.isEmpty(folderIds)) { + return Collections.emptyList(); + } + List roots = documentMapper.selectListByFolderIds(folderIds); + if (CollUtil.isEmpty(roots)) { + return Collections.emptyList(); + } + List documents = new ArrayList<>(roots); + Set visitedDocumentIds = new LinkedHashSet<>(convertSet(roots, PmsKnowledgeDocumentDO::getId)); + Set parentIds = convertSet(roots, PmsKnowledgeDocumentDO::getId); + + // 2. 逐层查询子文档;使用深度上限和已访问集合共同防御异常循环数据 + for (int depth = 0; depth < Short.MAX_VALUE && CollUtil.isNotEmpty(parentIds); depth++) { + List childDocuments = documentMapper.selectListByParentIds(parentIds); + List newChildDocuments = new ArrayList<>(); + for (PmsKnowledgeDocumentDO childDocument : childDocuments) { + if (visitedDocumentIds.add(childDocument.getId())) { + newChildDocuments.add(childDocument); + } + } + if (CollUtil.isEmpty(newChildDocuments)) { + break; + } + documents.addAll(newChildDocuments); + parentIds = convertSet(newChildDocuments, PmsKnowledgeDocumentDO::getId); + } + return documents; + } + + @Override + public void moveDocumentList(Collection documents, Long targetLibraryId, + Map permissionIdMap) { + if (CollUtil.isEmpty(documents)) { + return; + } + for (PmsKnowledgeDocumentDO document : documents) { + document.setLibraryId(targetLibraryId); + if (CollUtil.isNotEmpty(permissionIdMap)) { + document.setPermissionId(permissionIdMap.get(document.getPermissionId())); + } + } + documentMapper.updateBatch(documents); + } + + @Override + public PageResult getDocumentSearchPage( + PmsKnowledgeDocumentSearchPageReqVO pageReqVO, Long userId) { + // 1. 查询当前用户可读的知识库和内容权限 + List readableLibraryIds = libraryMemberService.getReadableLibraryIdList(userId); + if (CollUtil.isEmpty(readableLibraryIds) + || pageReqVO.getLibraryId() != null && !readableLibraryIds.contains(pageReqVO.getLibraryId())) { + return PageResult.empty(); + } + Set readablePermissionIds = contentPermissionService.getReadableContentPermissionIdSet(readableLibraryIds, userId); + if (CollUtil.isEmpty(readablePermissionIds)) { + return PageResult.empty(); + } + + // 2. 使用 MySQL 对标题和正文进行模糊搜索 + return documentMapper.selectPage(pageReqVO, readableLibraryIds, readablePermissionIds, + PmsKnowledgeDocumentStatusEnum.ACTIVE_STATUSES); + } + + @Override + public List getDocumentList(Long libraryId, Long userId) { + // 1. 校验知识库可读 + libraryMemberService.validateLibraryReadable(libraryId, userId); + + // 2. 查询全部正常文档 + List documents = documentMapper.selectListByLibraryIdAndStatus( + libraryId, PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + if (CollUtil.isEmpty(documents)) { + return Collections.emptyList(); + } + + // 3. 按内容权限过滤,只返回当前用户有读取权限的文档 + Map permissionLevelMap = contentPermissionService.getCurrentUserContentPermissionLevelMap( + convertSet(documents, PmsKnowledgeDocumentDO::getPermissionId), libraryId, userId); + documents.removeIf(document -> permissionLevelMap.get(document.getPermissionId()) == null); + + // 4. 父文档不可见时,整条子文档分支均不可见 + Map> childDocumentIdMap = new LinkedHashMap<>(); + Set visibleDocumentIds = new LinkedHashSet<>(); + Deque pendingDocumentIds = new ArrayDeque<>(); + for (PmsKnowledgeDocumentDO document : documents) { + if (PmsKnowledgeDocumentDO.PARENT_ID_ROOT.equals(document.getParentId())) { + visibleDocumentIds.add(document.getId()); + pendingDocumentIds.add(document.getId()); + } else { + childDocumentIdMap.computeIfAbsent(document.getParentId(), key -> new ArrayList<>()) + .add(document.getId()); + } + } + while (CollUtil.isNotEmpty(pendingDocumentIds)) { + Long parentId = pendingDocumentIds.removeFirst(); + for (Long childId : childDocumentIdMap.getOrDefault(parentId, Collections.emptyList())) { + if (visibleDocumentIds.add(childId)) { + pendingDocumentIds.addLast(childId); + } + } + } + documents.removeIf(document -> !visibleDocumentIds.contains(document.getId())); + return documents; + } + + @Override + public Map> getDocumentTypeCountMap(Collection libraryIds) { + return CollUtil.isEmpty(libraryIds) ? Collections.emptyMap() + : documentMapper.selectTypeCountMapByLibraryIdsAndStatus( + libraryIds, PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + } + + private PmsKnowledgeDocumentDO validateDocumentExists(Long id) { + PmsKnowledgeDocumentDO document = documentMapper.selectById(id); + if (document == null || PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus().equals(document.getStatus())) { + throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS); + } + return document; + } + + /** + * 规范并校验文档目录位置,返回继承的内容权限编号 + * + * @param placement 文档位置 + * @param document 待移动文档;创建时为空 + * @param userId 用户编号 + * @return 继承的内容权限编号;位于知识库根目录时返回 {@code null} + */ + private Long normalizeAndValidateDocumentPlacement(PmsKnowledgeDocumentDO placement, + PmsKnowledgeDocumentDO document, + Long userId) { + // 1. 文件夹优先于父文档,文档放入文件夹根节点并继承文件夹权限 + if (placement.getFolderId() != null + && notEqual(PmsKnowledgeDocumentDO.FOLDER_ID_ROOT, placement.getFolderId())) { + placement.setParentId(PmsKnowledgeDocumentDO.PARENT_ID_ROOT); + PmsKnowledgeFolderDO folder = folderService.getFolder(placement.getFolderId(), userId); + if (notEqual(placement.getLibraryId(), folder.getLibraryId()) + || notEqual(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus(), folder.getStatus())) { + throw exception(KNOWLEDGE_DOCUMENT_FOLDER_INVALID); + } + if (document == null) { + contentPermissionService.validateContentPermissionWritable( + folder.getPermissionId(), folder.getLibraryId(), userId); + } else { + contentPermissionService.validateContentPermissionManageable( + folder.getPermissionId(), folder.getLibraryId(), userId); + } + return folder.getPermissionId(); + } + + // 2. 没有指定文件夹和父文档时,规范到知识库根目录 + placement.setFolderId(PmsKnowledgeDocumentDO.FOLDER_ID_ROOT); + if (placement.getParentId() == null + || PmsKnowledgeDocumentDO.PARENT_ID_ROOT.equals(placement.getParentId())) { + placement.setParentId(PmsKnowledgeDocumentDO.PARENT_ID_ROOT); + if (document == null) { + libraryMemberService.validateLibraryWritable(placement.getLibraryId(), userId); + } else { + libraryMemberService.validateLibraryAdmin(placement.getLibraryId(), userId); + } + return null; + } + + // 3.1 校验父文档存在 + PmsKnowledgeDocumentDO parent = documentMapper.selectById(placement.getParentId()); + if (parent == null) { + throw exception(KNOWLEDGE_DOCUMENT_PARENT_INVALID); + } + // 3.2 校验父文档属于目标知识库 + if (notEqual(placement.getLibraryId(), parent.getLibraryId())) { + throw exception(KNOWLEDGE_DOCUMENT_PARENT_INVALID); + } + // 3.3 校验父文档处于正常状态 + if (notEqual(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus(), parent.getStatus())) { + throw exception(KNOWLEDGE_DOCUMENT_PARENT_INVALID); + } + // 3.4 校验文档不能移动到自身或子文档下 + if (document != null) { + List documents = getDocumentDescendantList(document); + Set documentIds = convertSet(documents, PmsKnowledgeDocumentDO::getId); + if (documentIds.contains(placement.getParentId())) { + throw exception(KNOWLEDGE_DOCUMENT_MOVE_INVALID); + } + } + // 3.5 创建时校验可编辑,移动时校验目标父文档管理权限 + if (document == null) { + contentPermissionService.validateContentPermissionWritable( + parent.getPermissionId(), parent.getLibraryId(), userId); + } else { + contentPermissionService.validateContentPermissionManageable( + parent.getPermissionId(), parent.getLibraryId(), userId); + } + return parent.getPermissionId(); + } + + /** + * 获得根文档及全部子文档列表 + * + * @param root 根文档 + * @return 根文档及全部子文档列表 + */ + private List getDocumentDescendantList(PmsKnowledgeDocumentDO root) { + // 1. 初始化结果、已访问文档编号和首层父文档编号 + List documents = new ArrayList<>(); + documents.add(root); + Set visitedDocumentIds = new LinkedHashSet<>(); + visitedDocumentIds.add(root.getId()); + Collection parentIds = Collections.singleton(root.getId()); + + // 2. 逐层查询子文档,并通过已访问文档编号避免异常环路和重复数据 + for (int i = 0; i < Short.MAX_VALUE; i++) { + List childDocuments = documentMapper.selectListByParentIds(parentIds); + childDocuments.removeIf(child -> !visitedDocumentIds.add(child.getId())); + if (CollUtil.isEmpty(childDocuments)) { + break; + } + documents.addAll(childDocuments); + parentIds = convertSet(childDocuments, PmsKnowledgeDocumentDO::getId); + } + return documents; + } + + @Override + public Set getExistingContentPermissionIdSet(Collection permissionIds) { + if (CollUtil.isEmpty(permissionIds)) { + return Collections.emptySet(); + } + return documentMapper.selectExistingPermissionIdSet(permissionIds); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderService.java new file mode 100644 index 000000000..2e529337b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderService.java @@ -0,0 +1,122 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * PMS 知识库文件夹 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeFolderService { + + /** + * 创建文件夹 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @return 文件夹编号 + */ + Long createFolder(PmsKnowledgeFolderSaveReqVO saveReqVO, Long userId); + + /** + * 更新文件夹名称 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + */ + void updateFolder(PmsKnowledgeFolderSaveReqVO saveReqVO, Long userId); + + /** + * 删除文件夹,并将文件夹子树移入回收站 + * + * @param id 文件夹编号 + * @param userId 用户编号 + */ + void deleteFolder(Long id, Long userId); + + /** + * 移动文件夹到目标目录,跨知识库移动时复制内容权限 + * + * @param moveReqVO 移动信息 + * @param userId 用户编号 + */ + void moveFolder(PmsKnowledgeFolderMoveReqVO moveReqVO, Long userId); + + /** + * 获得文件夹,校验当前用户有内容读取权限 + * + * @param id 文件夹编号 + * @param userId 用户编号 + * @return 文件夹 + */ + PmsKnowledgeFolderDO getFolder(Long id, Long userId); + + /** + * 获得文件夹 + * + * @param id 文件夹编号 + * @return 文件夹 + */ + PmsKnowledgeFolderDO getFolder(Long id); + + /** + * 获得指定编号的文件夹列表 + * + * @param ids 文件夹编号集合 + * @return 文件夹列表 + */ + List getFolderList(Collection ids); + + /** + * 获得知识库的全部文件夹列表 + * + * @param libraryId 知识库编号 + * @return 文件夹列表 + */ + List getFolderListByLibraryId(Long libraryId); + + /** + * 批量更新文件夹 + * + * @param folders 文件夹列表 + */ + void updateFolderList(Collection folders); + + /** + * 批量恢复文件夹 + * + * @param ids 文件夹编号集合 + */ + void restoreFolderList(Collection ids); + + /** + * 批量彻底删除文件夹 + * + * @param ids 文件夹编号集合 + */ + void deleteFolderList(Collection ids); + + /** + * 获得知识库的文件夹列表,仅返回当前用户有内容读取权限的文件夹 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 文件夹列表 + */ + List getFolderList(Long libraryId, Long userId); + + /** + * 获得仍被文件夹引用的内容权限编号集合 + * + * @param permissionIds 内容权限编号集合 + * @return 被引用的内容权限编号集合 + */ + Set getExistingContentPermissionIdSet(Collection permissionIds); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderServiceImpl.java new file mode 100644 index 000000000..3c357535f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderServiceImpl.java @@ -0,0 +1,319 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeFolderMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.recycle.PmsKnowledgeRecycleService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_FOLDER_MOVE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_FOLDER_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_FOLDER_PARENT_INVALID; + +/** + * PMS 知识库文件夹 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeFolderServiceImpl implements PmsKnowledgeFolderService { + + @Resource + private PmsKnowledgeFolderMapper folderMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeDocumentService documentService; + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeFavoriteService favoriteService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeViewRecordService viewRecordService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeRecycleService recycleService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createFolder(PmsKnowledgeFolderSaveReqVO saveReqVO, Long userId) { + // 1.1 校验知识库读取权限 + libraryMemberService.validateLibraryReadable(saveReqVO.getLibraryId(), userId); + // 1.2 校验父文件夹存在 + PmsKnowledgeFolderDO parent = validateFolderParent(saveReqVO.getLibraryId(), saveReqVO.getParentId()); + Long permissionId; + if (parent == null) { + // 1.3 根目录下校验知识库编辑权限 + libraryMemberService.validateLibraryWritable(saveReqVO.getLibraryId(), userId); + permissionId = contentPermissionService.createDefaultContentPermission(saveReqVO.getLibraryId(), userId); + } else { + // 1.3 子目录下校验父文件夹编辑权限 + contentPermissionService.validateContentPermissionWritable(parent.getPermissionId(), parent.getLibraryId(), userId); + permissionId = parent.getPermissionId(); + } + + // 2. 创建文件夹 + PmsKnowledgeFolderDO folder = BeanUtils.toBean(saveReqVO, PmsKnowledgeFolderDO.class) + .setPermissionId(permissionId) + .setStatus(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + folderMapper.insert(folder); + return folder.getId(); + } + + @Override + public void updateFolder(PmsKnowledgeFolderSaveReqVO saveReqVO, Long userId) { + // 1.1 校验文件夹存在 + PmsKnowledgeFolderDO folder = validateFolderExists(saveReqVO.getId()); + // 1.2 校验所属知识库可读 + libraryMemberService.validateLibraryReadable(folder.getLibraryId(), userId); + // 1.3 校验内容编辑权限 + contentPermissionService.validateContentPermissionWritable(folder.getPermissionId(), folder.getLibraryId(), userId); + // 1.4 校验文件夹属于请求知识库 + if (ObjectUtil.notEqual(folder.getLibraryId(), saveReqVO.getLibraryId())) { + throw exception(KNOWLEDGE_FOLDER_PARENT_INVALID); + } + + // 2. 更新文件夹名称;目录位置通过移动接口维护 + folderMapper.updateById(BeanUtils.toBean(saveReqVO, PmsKnowledgeFolderDO.class) + .setId(folder.getId()).setLibraryId(null).setParentId(null)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteFolder(Long id, Long userId) { + // 1.1 校验文件夹存在 + PmsKnowledgeFolderDO folder = validateFolderExists(id); + // 1.2 校验所属知识库可读 + libraryMemberService.validateLibraryReadable(folder.getLibraryId(), userId); + // 1.3 校验内容删除权限 + contentPermissionService.validateContentPermissionDeletable(folder.getPermissionId(), folder.getLibraryId(), userId); + + // 2. 获得文件夹、子文件夹及其中的文档 + List folders = getFolderDescendants(folder); + List documents = documentService.getDocumentListByFolderIds( + convertSet(folders, PmsKnowledgeFolderDO::getId)); + + // 3. 将文件夹子树移入回收站 + recycleService.recycleFolder(folder, folders, documents, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void moveFolder(PmsKnowledgeFolderMoveReqVO moveReqVO, Long userId) { + // 1.1 校验源文件夹存在 + PmsKnowledgeFolderDO folder = validateFolderExists(moveReqVO.getId()); + // 1.2 校验源知识库和目标知识库可读 + libraryMemberService.validateLibraryReadable(folder.getLibraryId(), userId); + libraryMemberService.validateLibraryReadable(moveReqVO.getTargetLibraryId(), userId); + // 1.3 移动会改变目录结构,校验源文件夹管理权限 + contentPermissionService.validateContentPermissionManageable( + folder.getPermissionId(), folder.getLibraryId(), userId); + // 1.4 校验目标父文件夹存在 + PmsKnowledgeFolderDO targetParent = validateFolderParent(moveReqVO.getTargetLibraryId(), + moveReqVO.getTargetParentId()); + if (targetParent == null) { + // 1.5 根目录下校验目标知识库管理权限 + libraryMemberService.validateLibraryAdmin(moveReqVO.getTargetLibraryId(), userId); + } else { + // 1.5 子目录下校验目标文件夹管理权限 + contentPermissionService.validateContentPermissionManageable( + targetParent.getPermissionId(), targetParent.getLibraryId(), userId); + } + + // 2. 禁止移动到自身或子文件夹 + List folders = getFolderDescendants(folder); + Set folderIds = convertSet(folders, PmsKnowledgeFolderDO::getId); + if (folderIds.contains(moveReqVO.getTargetParentId())) { + throw exception(KNOWLEDGE_FOLDER_MOVE_INVALID); + } + + // 3. 跨知识库移动时复制权限,避免与原知识库中复用同一权限的内容相互影响 + List documents = documentService.getDocumentListByFolderIds(folderIds); + Set sourcePermissionIds = new LinkedHashSet<>(convertSet(folders, PmsKnowledgeFolderDO::getPermissionId)); + sourcePermissionIds.addAll(convertSet(documents, PmsKnowledgeDocumentDO::getPermissionId)); + Map clonedPermissionIdMap = ObjectUtil.equal(folder.getLibraryId(), moveReqVO.getTargetLibraryId()) + ? Collections.emptyMap() + : contentPermissionService.cloneContentPermissions(sourcePermissionIds, moveReqVO.getTargetLibraryId()); + boolean crossLibrary = ObjectUtil.notEqual(folder.getLibraryId(), moveReqVO.getTargetLibraryId()); + + // 4. 更新目录位置,并同步子文件夹和文档所属知识库与权限 + for (PmsKnowledgeFolderDO item : folders) { + item.setLibraryId(moveReqVO.getTargetLibraryId()); + if (CollUtil.isNotEmpty(clonedPermissionIdMap)) { + item.setPermissionId(clonedPermissionIdMap.get(item.getPermissionId())); + } + if (item.getId().equals(folder.getId())) { + item.setParentId(moveReqVO.getTargetParentId()); + } + } + if (CollUtil.isNotEmpty(folders)) { + folderMapper.updateBatch(folders); + } + documentService.moveDocumentList(documents, moveReqVO.getTargetLibraryId(), clonedPermissionIdMap); + + // 5. 跨知识库移动时同步关注和最近浏览记录的当前归属 + if (crossLibrary) { + Set documentIds = convertSet(documents, PmsKnowledgeDocumentDO::getId); + favoriteService.updateFavoriteLibraryIdByEntityIds(folderIds, documentIds, + moveReqVO.getTargetLibraryId()); + viewRecordService.updateViewRecordLibraryIdByEntityIds(folderIds, documentIds, + moveReqVO.getTargetLibraryId()); + } + if (CollUtil.isNotEmpty(clonedPermissionIdMap)) { + contentPermissionService.deleteUnusedContentPermissions(sourcePermissionIds); + } + } + + @Override + public PmsKnowledgeFolderDO getFolder(Long id, Long userId) { + // 1.1 校验文件夹存在 + PmsKnowledgeFolderDO folder = validateFolderExists(id); + // 1.2 校验所属知识库可读 + libraryMemberService.validateLibraryReadable(folder.getLibraryId(), userId); + // 1.3 校验内容读取权限 + contentPermissionService.validateContentPermissionReadable(folder.getPermissionId(), folder.getLibraryId(), userId); + return folder; + } + + @Override + public PmsKnowledgeFolderDO getFolder(Long id) { + return folderMapper.selectById(id); + } + + @Override + public List getFolderList(Collection ids) { + return CollUtil.isEmpty(ids) ? Collections.emptyList() : folderMapper.selectByIds(ids); + } + + @Override + public List getFolderListByLibraryId(Long libraryId) { + return folderMapper.selectListByLibraryId(libraryId); + } + + @Override + public void updateFolderList(Collection folders) { + if (CollUtil.isEmpty(folders)) { + return; + } + folderMapper.updateBatch(folders); + } + + @Override + public void restoreFolderList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + folderMapper.updateToRestoreByIds(ids, PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + } + + @Override + public void deleteFolderList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + folderMapper.deleteByIds(ids); + } + + @Override + public List getFolderList(Long libraryId, Long userId) { + // 1. 校验知识库可读 + libraryMemberService.validateLibraryReadable(libraryId, userId); + // 2. 查询全部正常文件夹 + List folders = folderMapper.selectListByLibraryIdAndStatus( + libraryId, PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + // 3. 按内容权限过滤,只返回当前用户有读取权限的文件夹 + Set readablePermissionIds = new LinkedHashSet<>(); + contentPermissionService.getCurrentUserContentPermissionLevelMap(convertSet(folders, PmsKnowledgeFolderDO::getPermissionId), + libraryId, userId).forEach((permissionId, level) -> { + if (level != null) { + readablePermissionIds.add(permissionId); + } + }); + folders.removeIf(folder -> !readablePermissionIds.contains(folder.getPermissionId())); + return folders; + } + + private PmsKnowledgeFolderDO validateFolderExists(Long id) { + PmsKnowledgeFolderDO folder = folderMapper.selectById(id); + if (folder == null || PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus().equals(folder.getStatus())) { + throw exception(KNOWLEDGE_FOLDER_NOT_EXISTS); + } + return folder; + } + + private PmsKnowledgeFolderDO validateFolderParent(Long libraryId, Long parentId) { + if (parentId == null || PmsKnowledgeFolderDO.PARENT_ID_ROOT.equals(parentId)) { + return null; + } + PmsKnowledgeFolderDO parent = folderMapper.selectById(parentId); + if (parent == null || ObjectUtil.notEqual(libraryId, parent.getLibraryId()) + || !PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus().equals(parent.getStatus())) { + throw exception(KNOWLEDGE_FOLDER_PARENT_INVALID); + } + return parent; + } + + /** + * 获得根文件夹及其全部子文件夹,使用 visited 集合避免异常环形数据导致循环 + */ + private List getFolderDescendants(PmsKnowledgeFolderDO root) { + List folders = new ArrayList<>(); + folders.add(root); + Set visitedFolderIds = new LinkedHashSet<>(); + visitedFolderIds.add(root.getId()); + Set parentIds = Collections.singleton(root.getId()); + while (CollUtil.isNotEmpty(parentIds)) { + List childFolders = folderMapper.selectListByParentIds(parentIds); + List newChildFolders = new ArrayList<>(); + for (PmsKnowledgeFolderDO childFolder : childFolders) { + if (visitedFolderIds.add(childFolder.getId())) { + newChildFolders.add(childFolder); + } + } + if (CollUtil.isEmpty(newChildFolders)) { + break; + } + folders.addAll(newChildFolders); + parentIds = convertSet(newChildFolders, PmsKnowledgeFolderDO::getId); + } + return folders; + } + + @Override + public Set getExistingContentPermissionIdSet(Collection permissionIds) { + if (CollUtil.isEmpty(permissionIds)) { + return Collections.emptySet(); + } + return folderMapper.selectExistingPermissionIdSet(permissionIds); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentService.java new file mode 100644 index 000000000..8469cc3a7 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentService.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.comment.PmsKnowledgeDocumentCommentSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentCommentDO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 知识库文档评论 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeDocumentCommentService { + + /** + * 创建文档评论或回复 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @return 评论编号 + */ + Long createDocumentComment(PmsKnowledgeDocumentCommentSaveReqVO saveReqVO, Long userId); + + /** + * 删除自己的评论,删除主评论时同步删除全部回复 + * + * @param id 评论编号 + * @param userId 用户编号 + */ + void deleteDocumentComment(Long id, Long userId); + + /** + * 获得文档的评论和回复列表 + * + * @param documentId 文档编号 + * @param userId 用户编号 + * @return 评论列表 + */ + List getDocumentCommentList(Long documentId, Long userId); + + /** + * 删除文档的全部评论 + * + * @param documentIds 文档编号集合 + */ + void deleteCommentsByDocumentIds(Collection documentIds); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentServiceImpl.java new file mode 100644 index 000000000..33382d374 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentServiceImpl.java @@ -0,0 +1,120 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.comment.PmsKnowledgeDocumentCommentSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentCommentDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeDocumentCommentMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_COMMENT_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_COMMENT_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_COMMENT_REPLY_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_COMMENT_TYPE_INVALID; + +/** + * PMS 知识库文档评论 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeDocumentCommentServiceImpl implements PmsKnowledgeDocumentCommentService { + + @Resource + private PmsKnowledgeDocumentCommentMapper commentMapper; + + @Resource + @Lazy // 延迟加载,避免与文档回收逻辑形成循环依赖 + private PmsKnowledgeDocumentService documentService; + + @Override + public Long createDocumentComment(PmsKnowledgeDocumentCommentSaveReqVO saveReqVO, Long userId) { + // 1.1 校验文档可评论 + validateDocumentCommentable(saveReqVO.getDocumentId(), userId); + // 1.2 校验回复关系 + Long mainId = saveReqVO.getMainId() != null ? saveReqVO.getMainId() + : PmsKnowledgeDocumentCommentDO.MAIN_ID_ROOT; + if (ObjectUtil.notEqual(PmsKnowledgeDocumentCommentDO.MAIN_ID_ROOT, mainId)) { + PmsKnowledgeDocumentCommentDO mainComment = validateCommentExists(mainId); + if (ObjectUtil.notEqual(saveReqVO.getDocumentId(), mainComment.getDocumentId()) + || ObjectUtil.notEqual(PmsKnowledgeDocumentCommentDO.MAIN_ID_ROOT, mainComment.getMainId())) { + throw exception(KNOWLEDGE_DOCUMENT_COMMENT_REPLY_INVALID); + } + } + + // 2. 创建评论 + PmsKnowledgeDocumentCommentDO comment = BeanUtils.toBean(saveReqVO, PmsKnowledgeDocumentCommentDO.class) + .setUserId(userId).setMainId(mainId); + commentMapper.insert(comment); + return comment.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDocumentComment(Long id, Long userId) { + // 1.1 校验评论存在 + PmsKnowledgeDocumentCommentDO comment = validateCommentExists(id); + // 1.2 校验文档可评论 + validateDocumentCommentable(comment.getDocumentId(), userId); + // 1.3 校验评论属于当前用户 + validateCommentOwner(comment, userId); + + // 2. 删除主评论时同步删除全部回复 + if (PmsKnowledgeDocumentCommentDO.MAIN_ID_ROOT.equals(comment.getMainId())) { + commentMapper.deleteByMainId(comment.getId()); + } + commentMapper.deleteById(comment.getId()); + } + + @Override + public List getDocumentCommentList(Long documentId, Long userId) { + // 1. 校验文档可访问 + validateDocumentCommentable(documentId, userId); + + // 2. 查询评论和回复 + return commentMapper.selectListByDocumentId(documentId); + } + + @Override + public void deleteCommentsByDocumentIds(Collection documentIds) { + if (CollUtil.isEmpty(documentIds)) { + return; + } + commentMapper.deleteByDocumentIds(documentIds); + } + + private PmsKnowledgeDocumentCommentDO validateCommentExists(Long id) { + PmsKnowledgeDocumentCommentDO comment = commentMapper.selectById(id); + if (comment == null) { + throw exception(KNOWLEDGE_DOCUMENT_COMMENT_NOT_EXISTS); + } + return comment; + } + + private void validateCommentOwner(PmsKnowledgeDocumentCommentDO comment, Long userId) { + if (ObjectUtil.notEqual(userId, comment.getUserId())) { + throw exception(KNOWLEDGE_DOCUMENT_COMMENT_ACCESS_DENIED); + } + } + + private void validateDocumentCommentable(Long documentId, Long userId) { + PmsKnowledgeDocumentDO document = documentService.getDocument(documentId, userId); + if (!PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType().equals(document.getType())) { + throw exception(KNOWLEDGE_DOCUMENT_COMMENT_TYPE_INVALID); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeService.java new file mode 100644 index 000000000..20140c46b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeService.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentLikeDO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 知识文档点赞 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeDocumentLikeService { + + /** + * 点赞文档 + * + * @param documentId 文档编号 + * @param userId 用户编号 + */ + void createDocumentLike(Long documentId, Long userId); + + /** + * 取消点赞文档 + * + * @param documentId 文档编号 + * @param userId 用户编号 + */ + void deleteDocumentLike(Long documentId, Long userId); + + /** + * 获得文档的点赞列表 + * + * @param documentId 文档编号 + * @return 点赞列表 + */ + List getDocumentLikeList(Long documentId); + + /** + * 删除文档的点赞关系 + * + * @param documentIds 文档编号集合 + */ + void deleteLikesByDocumentIds(Collection documentIds); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeServiceImpl.java new file mode 100644 index 000000000..de9993ee0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeServiceImpl.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentLikeDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeDocumentLikeMapper; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 知识文档点赞 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeDocumentLikeServiceImpl implements PmsKnowledgeDocumentLikeService { + + @Resource + private PmsKnowledgeDocumentLikeMapper documentLikeMapper; + @Resource + private PmsKnowledgeInteractionTargetService interactionTargetService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createDocumentLike(Long documentId, Long userId) { + // 1. 校验文档可读 + interactionTargetService.validateTargetReadable( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), documentId, userId); + + // 2. 新增点赞关系,重复请求保持幂等 + PmsKnowledgeDocumentLikeDO like = documentLikeMapper.selectByDocumentIdAndUserId(documentId, userId); + if (like != null) { + return; + } + documentLikeMapper.insert(new PmsKnowledgeDocumentLikeDO().setDocumentId(documentId).setUserId(userId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDocumentLike(Long documentId, Long userId) { + // 1. 校验文档可读 + interactionTargetService.validateTargetReadable( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), documentId, userId); + + // 2. 删除点赞关系,重复请求保持幂等 + PmsKnowledgeDocumentLikeDO like = documentLikeMapper.selectByDocumentIdAndUserId(documentId, userId); + if (like != null) { + documentLikeMapper.deleteById(like.getId()); + } + } + + @Override + public List getDocumentLikeList(Long documentId) { + return documentLikeMapper.selectListByDocumentId(documentId); + } + + @Override + public void deleteLikesByDocumentIds(Collection documentIds) { + if (CollUtil.isEmpty(documentIds)) { + return; + } + documentLikeMapper.deleteByDocumentIds(documentIds); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareService.java new file mode 100644 index 000000000..dc1514d8e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareService.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentShareDO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 知识库文档分享 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeDocumentShareService { + + /** + * 开启文档分享,重新开启时生成新的外部令牌,并通知内部分享成员 + * + * @param documentId 文档编号 + * @param shareUserIds 内部分享用户编号列表 + * @param userId 用户编号 + * @return 分享信息 + */ + PmsKnowledgeDocumentShareDO openShare(Long documentId, List shareUserIds, Long userId); + + /** + * 更新文档的内部分享成员,只通知本次新增的成员 + * + * @param documentId 文档编号 + * @param shareUserIds 内部分享用户编号列表 + * @param userId 用户编号 + */ + void updateShareMemberList(Long documentId, List shareUserIds, Long userId); + + /** + * 关闭文档分享 + * + * @param documentId 文档编号 + * @param userId 用户编号 + */ + void closeShare(Long documentId, Long userId); + + /** + * 获得文档处于开启状态的分享 + * + * @param documentId 文档编号 + * @return 分享信息 + */ + PmsKnowledgeDocumentShareDO getActiveDocumentShare(Long documentId); + + /** + * 获得文档处于开启状态的分享,校验当前用户有读取权限 + * + * @param documentId 文档编号 + * @param userId 用户编号 + * @return 分享信息 + */ + PmsKnowledgeDocumentShareDO getActiveDocumentShare(Long documentId, Long userId); + + /** + * 通过分享令牌获得正常状态的文档 + * + * @param token 分享令牌 + * @return 文档 + */ + PmsKnowledgeDocumentDO getDocumentByShareToken(String token); + + /** + * 删除文档的分享记录 + * + * @param documentIds 文档编号集合 + */ + void deleteSharesByDocumentIds(Collection documentIds); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareServiceImpl.java new file mode 100644 index 000000000..78ff8c835 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareServiceImpl.java @@ -0,0 +1,244 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentShareDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeDocumentShareMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.google.common.collect.Maps; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_SHARE_ALREADY_OPEN; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_SHARE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.MessageTemplateConstants.KNOWLEDGE_DOCUMENT_SHARED; + +/** + * PMS 知识库文档分享 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class PmsKnowledgeDocumentShareServiceImpl implements PmsKnowledgeDocumentShareService { + + @Resource + private PmsKnowledgeDocumentShareMapper documentShareMapper; + @Resource + private PmsKnowledgeDocumentMapper documentMapper; + + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + @Resource + private AdminUserApi adminUserApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public PmsKnowledgeDocumentShareDO openShare(Long documentId, List shareUserIds, Long userId) { + // 1.1 校验文档存在 + PmsKnowledgeDocumentDO document = validateDocumentExists(documentId); + // 1.2 校验文档内容编辑权限 + contentPermissionService.validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId); + // 1.3 校验文档尚未开启分享 + PmsKnowledgeDocumentShareDO share = documentShareMapper.selectByDocumentId(documentId); + if (share != null && CommonStatusEnum.isEnable(share.getStatus())) { + throw exception(KNOWLEDGE_DOCUMENT_SHARE_ALREADY_OPEN); + } + + // 1.4 校验并规范分享成员 + List normalizedShareUserIds = normalizeShareUserIds(shareUserIds); + + // 2. 新建或重新开启分享;重新开启时生成新的外部令牌 + if (share == null) { + share = new PmsKnowledgeDocumentShareDO().setDocumentId(documentId); + } + share.setShareUserIds(normalizedShareUserIds).setToken(generateShareToken()) + .setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setCloseUserId(null).setCloseTime(null); + if (share.getId() == null) { + documentShareMapper.insert(share); + } else { + documentShareMapper.updateToReopen(share); + } + + // 3. 通知内部分享成员 + sendKnowledgeDocumentSharedNotify(normalizedShareUserIds, userId, share.getToken(), document.getTitle()); + return share; + } + + @Override + public void updateShareMemberList(Long documentId, List shareUserIds, Long userId) { + // 1.1 校验文档存在 + PmsKnowledgeDocumentDO document = validateDocumentExists(documentId); + // 1.2 校验文档内容编辑权限 + contentPermissionService.validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId); + // 1.3 校验文档已开启分享 + PmsKnowledgeDocumentShareDO share = validateActiveShare(documentId); + + // 1.4 校验并规范分享成员 + List normalizedShareUserIds = normalizeShareUserIds(shareUserIds); + + // 2. 更新内部分享成员 + documentShareMapper.updateById(new PmsKnowledgeDocumentShareDO().setId(share.getId()) + .setShareUserIds(normalizedShareUserIds)); + + // 3. 只通知本次新增的分享成员 + Set addedUserIds = new LinkedHashSet<>(normalizedShareUserIds); + if (CollUtil.isNotEmpty(share.getShareUserIds())) { + share.getShareUserIds().forEach(addedUserIds::remove); + } + sendKnowledgeDocumentSharedNotify(addedUserIds, userId, share.getToken(), document.getTitle()); + } + + @Override + public void closeShare(Long documentId, Long userId) { + // 1.1 校验文档存在 + PmsKnowledgeDocumentDO document = validateDocumentExists(documentId); + // 1.2 校验文档内容编辑权限 + contentPermissionService.validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId); + // 1.3 校验文档已开启分享 + PmsKnowledgeDocumentShareDO share = validateActiveShare(documentId); + + // 2. 关闭分享 + documentShareMapper.updateById(new PmsKnowledgeDocumentShareDO().setId(share.getId()) + .setStatus(CommonStatusEnum.DISABLE.getStatus()) + .setCloseUserId(userId).setCloseTime(LocalDateTime.now())); + } + + @Override + public PmsKnowledgeDocumentShareDO getActiveDocumentShare(Long documentId) { + PmsKnowledgeDocumentShareDO share = documentShareMapper.selectByDocumentId(documentId); + return share != null && CommonStatusEnum.isEnable(share.getStatus()) + ? share : null; + } + + @Override + public PmsKnowledgeDocumentShareDO getActiveDocumentShare(Long documentId, Long userId) { + // 1.1 校验文档存在 + PmsKnowledgeDocumentDO document = validateDocumentExists(documentId); + // 1.2 校验文档内容读取权限 + contentPermissionService.validateContentPermissionReadable( + document.getPermissionId(), document.getLibraryId(), userId); + // 2. 返回当前仍处于开启状态的分享配置 + return getActiveDocumentShare(documentId); + } + + @Override + public PmsKnowledgeDocumentDO getDocumentByShareToken(String token) { + // 1. 校验分享令牌处于开启状态 + PmsKnowledgeDocumentShareDO share = documentShareMapper.selectByToken(token); + if (share == null || CommonStatusEnum.isDisable(share.getStatus())) { + throw exception(KNOWLEDGE_DOCUMENT_SHARE_INVALID); + } + + // 2. 查询正常状态的分享文档 + PmsKnowledgeDocumentDO document = documentMapper.selectById(share.getDocumentId()); + if (document == null || ObjectUtil.notEqual(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus(), + document.getStatus())) { + throw exception(KNOWLEDGE_DOCUMENT_SHARE_INVALID); + } + return document; + } + + @Override + public void deleteSharesByDocumentIds(Collection documentIds) { + if (CollUtil.isEmpty(documentIds)) { + return; + } + documentShareMapper.deleteByDocumentIds(documentIds); + } + + private PmsKnowledgeDocumentDO validateDocumentExists(Long documentId) { + PmsKnowledgeDocumentDO document = documentMapper.selectById(documentId); + if (document == null || ObjectUtil.notEqual(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus(), + document.getStatus())) { + throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS); + } + return document; + } + + private PmsKnowledgeDocumentShareDO validateActiveShare(Long documentId) { + PmsKnowledgeDocumentShareDO share = getActiveDocumentShare(documentId); + if (share == null) { + throw exception(KNOWLEDGE_DOCUMENT_SHARE_INVALID); + } + return share; + } + + /** + * 规范并校验分享成员列表,避免重复通知并拒绝不存在或已禁用的用户 + */ + private List normalizeShareUserIds(List shareUserIds) { + if (CollUtil.isEmpty(shareUserIds)) { + adminUserApi.validateUserList(Collections.emptyList()).checkError(); + return Collections.emptyList(); + } + List normalizedShareUserIds = new ArrayList<>(new LinkedHashSet<>(shareUserIds)); + adminUserApi.validateUserList(normalizedShareUserIds).checkError(); + return normalizedShareUserIds; + } + + /** + * 生成外部分享令牌 + * + * @return 新的分享令牌 + */ + private String generateShareToken() { + return IdUtil.fastSimpleUUID(); + } + + /** + * 发送知识文档分享站内信。通知配置异常不能阻断文档分享主业务 + */ + private void sendKnowledgeDocumentSharedNotify(Collection receiverUserIds, Long operatorUserId, + String shareToken, String documentTitle) { + if (CollUtil.isEmpty(receiverUserIds)) { + return; + } + Set uniqueReceiverUserIds = new LinkedHashSet<>(receiverUserIds); + uniqueReceiverUserIds.remove(null); + uniqueReceiverUserIds.remove(operatorUserId); + Map templateParams = Maps.newHashMapWithExpectedSize(2); + templateParams.put("documentTitle", documentTitle); + templateParams.put("route", "/pms/kb/document/share/" + shareToken); + uniqueReceiverUserIds.forEach(receiverUserId -> { + try { + notifyMessageSendApi.sendSingleMessageToAdmin(new NotifySendSingleToUserReqDTO() + .setUserId(receiverUserId).setTemplateCode(KNOWLEDGE_DOCUMENT_SHARED) + .setTemplateParams(templateParams)).checkError(); + } catch (RuntimeException ex) { + log.warn("[sendKnowledgeDocumentSharedNotify][向用户({})发送知识文档分享站内信失败,参数为({})]", + receiverUserId, templateParams, ex); + } + }); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteService.java new file mode 100644 index 000000000..07a4a57e3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteService.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoritePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoriteSaveReqVO; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * PMS 知识收藏(关注)Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeFavoriteService { + + /** + * 关注知识对象 + * + * @param saveReqVO 关注信息 + * @param userId 用户编号 + */ + void createFavorite(PmsKnowledgeFavoriteSaveReqVO saveReqVO, Long userId); + + /** + * 取消关注,未关注时不做任何处理 + * + * @param type 对象类型 + * @param entityId 对象编号 + * @param userId 用户编号 + */ + void deleteFavorite(Integer type, Long entityId, Long userId); + + /** + * 获得当前用户的关注分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 关注分页结果 + */ + PageResult getFavoritePage(PmsKnowledgeFavoritePageReqVO pageReqVO, + Long userId); + + /** + * 获得指定知识库内当前用户的关注列表 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 关注列表 + */ + List getFavoriteListByLibraryId(Long libraryId, Long userId); + + /** + * 获得当前用户已关注的对象编号集合 + * + * @param type 对象类型 + * @param entityIds 对象编号集合 + * @param userId 用户编号 + * @return 已关注的对象编号集合 + */ + Set getFavoriteEntityIdSet(Integer type, Collection entityIds, Long userId); + + /** + * 判断当前用户是否已关注对象 + * + * @param type 对象类型 + * @param entityId 对象编号 + * @param userId 用户编号 + * @return 是否已关注 + */ + boolean isFavorite(Integer type, Long entityId, Long userId); + + /** + * 删除知识库的全部关注关系 + * + * @param libraryId 知识库编号 + */ + void deleteFavoritesByLibraryId(Long libraryId); + + /** + * 删除文件夹和文档的关注关系 + * + * @param folderIds 文件夹编号集合 + * @param documentIds 文档编号集合 + */ + void deleteFavoritesByEntityIds(Collection folderIds, Collection documentIds); + + /** + * 更新文件夹和文档关注关系的所属知识库 + * + * @param folderIds 文件夹编号集合 + * @param documentIds 文档编号集合 + * @param libraryId 目标知识库编号 + */ + void updateFavoriteLibraryIdByEntityIds(Collection folderIds, Collection documentIds, + Long libraryId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteServiceImpl.java new file mode 100644 index 000000000..b1d9b515e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteServiceImpl.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoritePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoriteSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeFavoriteDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeFavoriteMapper; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_FAVORITE_DUPLICATE; + +/** + * PMS 知识收藏(关注)Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeFavoriteServiceImpl implements PmsKnowledgeFavoriteService { + + @Resource + private PmsKnowledgeFavoriteMapper favoriteMapper; + @Resource + private PmsKnowledgeInteractionTargetService interactionTargetService; + + @Override + public void createFavorite(PmsKnowledgeFavoriteSaveReqVO saveReqVO, Long userId) { + // 1.1 校验关注对象可读 + Long libraryId = interactionTargetService.validateTargetReadable( + saveReqVO.getType(), saveReqVO.getEntityId(), userId); + // 1.2 校验关注关系不存在 + if (favoriteMapper.selectByUserIdAndTypeAndEntityId( + userId, saveReqVO.getType(), saveReqVO.getEntityId()) != null) { + throw exception(KNOWLEDGE_FAVORITE_DUPLICATE); + } + + // 2. 新增关注关系 + favoriteMapper.insert(BeanUtils.toBean(saveReqVO, PmsKnowledgeFavoriteDO.class) + .setLibraryId(libraryId).setUserId(userId)); + } + + @Override + public void deleteFavorite(Integer type, Long entityId, Long userId) { + // 1. 查询当前用户的关注关系 + PmsKnowledgeFavoriteDO favorite = favoriteMapper.selectByUserIdAndTypeAndEntityId(userId, type, entityId); + // 2. 存在时删除关注关系,未关注时不做任何处理 + if (favorite != null) { + favoriteMapper.deleteById(favorite.getId()); + } + } + + @Override + public PageResult getFavoritePage(PmsKnowledgeFavoritePageReqVO pageReqVO, + Long userId) { + // 1. 先批量判定全部关注对象的可读性 + List allItems = getReadableFavoriteList( + favoriteMapper.selectListByUserId(userId), userId); + if (CollUtil.isEmpty(allItems)) { + return PageResult.empty(); + } + Map itemMap = new LinkedHashMap<>(); + allItems.forEach(item -> itemMap.put(item.getId(), item)); + + // 2. 将类型、排序和分页交给数据库,再按页拼接已批量解析的展示对象 + PageResult favoritePage = favoriteMapper.selectPage( + pageReqVO, userId, itemMap.keySet()); + return new PageResult<>(convertList(favoritePage.getList(), favorite -> itemMap.get(favorite.getId())), + favoritePage.getTotal()); + } + + @Override + public List getFavoriteListByLibraryId(Long libraryId, Long userId) { + // 1. 查询指定知识库内当前用户的关注对象 + List favorites = favoriteMapper.selectListByUserIdAndLibraryId(userId, libraryId); + // 2. 复用统一的对象可读校验和展示字段组装 + return getReadableFavoriteList(favorites, userId); + } + + /** + * 将关注记录转换为当前用户可读取的互动对象 + * + * @param favorites 关注记录 + * @param userId 用户编号 + * @return 可读取的互动对象列表 + */ + private List getReadableFavoriteList( + List favorites, Long userId) { + if (CollUtil.isEmpty(favorites)) { + return Collections.emptyList(); + } + List items = convertList(favorites, favorite -> + new PmsKnowledgeInteractionItemRespVO().setId(favorite.getId()).setType(favorite.getType()) + .setEntityId(favorite.getEntityId()).setLibraryId(favorite.getLibraryId()) + .setCreateTime(favorite.getCreateTime())); + return interactionTargetService.getReadableItemList(items, userId); + } + + @Override + public Set getFavoriteEntityIdSet(Integer type, Collection entityIds, Long userId) { + if (CollUtil.isEmpty(entityIds)) { + return Collections.emptySet(); + } + List favorites = favoriteMapper + .selectListByUserIdAndTypeAndEntityIds(userId, type, entityIds); + return convertSet(favorites, PmsKnowledgeFavoriteDO::getEntityId); + } + + @Override + public boolean isFavorite(Integer type, Long entityId, Long userId) { + return favoriteMapper.selectByUserIdAndTypeAndEntityId(userId, type, entityId) != null; + } + + @Override + public void deleteFavoritesByLibraryId(Long libraryId) { + favoriteMapper.deleteByLibraryId(libraryId); + } + + @Override + public void deleteFavoritesByEntityIds(Collection folderIds, Collection documentIds) { + if (CollUtil.isNotEmpty(folderIds)) { + favoriteMapper.deleteByTypeAndEntityIds( + Collections.singleton(PmsKnowledgeObjectTypeEnum.FOLDER.getType()), folderIds); + } + if (CollUtil.isNotEmpty(documentIds)) { + favoriteMapper.deleteByTypeAndEntityIds(Arrays.asList(PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), + PmsKnowledgeObjectTypeEnum.FILE.getType()), documentIds); + } + } + + @Override + public void updateFavoriteLibraryIdByEntityIds(Collection folderIds, Collection documentIds, + Long libraryId) { + if (CollUtil.isNotEmpty(folderIds)) { + favoriteMapper.updateLibraryIdByTypeAndEntityIds(libraryId, + Collections.singleton(PmsKnowledgeObjectTypeEnum.FOLDER.getType()), folderIds); + } + if (CollUtil.isNotEmpty(documentIds)) { + favoriteMapper.updateLibraryIdByTypeAndEntityIds(libraryId, + Arrays.asList(PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), PmsKnowledgeObjectTypeEnum.FILE.getType()), documentIds); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFolderReadService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFolderReadService.java new file mode 100644 index 000000000..0cd1f208b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFolderReadService.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 知识库文件夹读取 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeFolderReadService { + + /** + * 获得当前用户可读的文件夹 + * + * @param id 文件夹编号 + * @param userId 用户编号 + * @return 文件夹 + */ + PmsKnowledgeFolderDO getReadableFolder(Long id, Long userId); + + /** + * 获得文件夹列表 + * + * @param ids 文件夹编号集合 + * @return 文件夹列表 + */ + List getFolderList(Collection ids); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFolderReadServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFolderReadServiceImpl.java new file mode 100644 index 000000000..8cb93cbfa --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFolderReadServiceImpl.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeFolderMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_FOLDER_NOT_EXISTS; + +/** + * PMS 知识库文件夹读取 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeFolderReadServiceImpl implements PmsKnowledgeFolderReadService { + + @Resource + private PmsKnowledgeFolderMapper folderMapper; + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + + @Override + public PmsKnowledgeFolderDO getReadableFolder(Long id, Long userId) { + // 1.1 校验文件夹存在 + PmsKnowledgeFolderDO folder = folderMapper.selectById(id); + if (folder == null) { + throw exception(KNOWLEDGE_FOLDER_NOT_EXISTS); + } + // 1.2 校验文件夹未被回收 + if (ObjectUtil.notEqual(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus(), folder.getStatus())) { + throw exception(KNOWLEDGE_FOLDER_NOT_EXISTS); + } + + // 2.1 校验知识库读取权限 + libraryMemberService.validateLibraryReadable(folder.getLibraryId(), userId); + // 2.2 校验内容读取权限 + contentPermissionService.validateContentPermissionReadable(folder.getPermissionId(), folder.getLibraryId(), userId); + return folder; + } + + @Override + public List getFolderList(Collection ids) { + return CollUtil.isEmpty(ids) ? Collections.emptyList() : folderMapper.selectByIds(ids); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetService.java new file mode 100644 index 000000000..a1c7394af --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetService.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 知识互动对象 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeInteractionTargetService { + + /** + * 校验互动对象可读 + * + * @param type 对象类型 + * @param entityId 对象编号 + * @param userId 用户编号 + * @return 知识库编号 + */ + Long validateTargetReadable(Integer type, Long entityId, Long userId); + + /** + * 批量获得当前用户可读的互动对象 + * + * @param targets 互动对象集合 + * @param userId 用户编号 + * @return 可读的互动对象列表 + */ + List getReadableItemList( + Collection targets, Long userId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetServiceImpl.java new file mode 100644 index 000000000..9cdccb124 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetServiceImpl.java @@ -0,0 +1,171 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_INTERACTION_OBJECT_INVALID; + +/** + * PMS 知识互动对象 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeInteractionTargetServiceImpl implements PmsKnowledgeInteractionTargetService { + + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + @Resource + private PmsKnowledgeLibraryService libraryService; + @Resource + private PmsKnowledgeFolderReadService folderReadService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeDocumentService documentService; + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + + @Override + public Long validateTargetReadable(Integer type, Long entityId, Long userId) { + // 1. 校验对象类型 + PmsKnowledgeObjectTypeEnum objectType = PmsKnowledgeObjectTypeEnum.valueOf(type); + if (objectType == null) { + throw exception(KNOWLEDGE_INTERACTION_OBJECT_INVALID); + } + + // 2. 校验对象可读 + if (PmsKnowledgeObjectTypeEnum.LIBRARY.equals(objectType)) { + return libraryMemberService.validateLibraryReadable(entityId, userId).getId(); + } + if (PmsKnowledgeObjectTypeEnum.FOLDER.equals(objectType)) { + return folderReadService.getReadableFolder(entityId, userId).getLibraryId(); + } + PmsKnowledgeDocumentDO document = documentService.getDocument(entityId, userId); + if (ObjectUtil.notEqual(type, document.getType())) { + throw exception(KNOWLEDGE_INTERACTION_OBJECT_INVALID); + } + return document.getLibraryId(); + } + + @Override + public List getReadableItemList( + Collection targets, Long userId) { + if (CollUtil.isEmpty(targets)) { + return Collections.emptyList(); + } + + // 1. 批量收集知识库、文件夹和文档编号 + Set libraryIds = new LinkedHashSet<>(); + Set folderIds = new LinkedHashSet<>(); + Set documentIds = new LinkedHashSet<>(); + for (PmsKnowledgeInteractionItemRespVO target : targets) { + libraryIds.add(target.getLibraryId()); + if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(target.getType())) { + folderIds.add(target.getEntityId()); + } else if (PmsKnowledgeObjectTypeEnum.DOCUMENT.getType().equals(target.getType()) + || PmsKnowledgeObjectTypeEnum.FILE.getType().equals(target.getType())) { + documentIds.add(target.getEntityId()); + } + } + + // 2. 批量查询对象,并计算当前用户的可读范围 + libraryIds.retainAll(libraryMemberService.getReadableLibraryIdList(userId)); + Map libraryMap = convertMap(libraryService.getLibraryList(libraryIds), + PmsKnowledgeLibraryDO::getId); + Map folderMap = convertMap(folderReadService.getFolderList(folderIds), + PmsKnowledgeFolderDO::getId); + Map documentMap = convertMap(documentService.getDocumentList(documentIds), + PmsKnowledgeDocumentDO::getId); + Set readablePermissionIds = contentPermissionService.getReadableContentPermissionIdSet(libraryIds, userId); + + // 3. 按原始顺序组装可读对象 + List items = new ArrayList<>(); + for (PmsKnowledgeInteractionItemRespVO target : targets) { + PmsKnowledgeInteractionItemRespVO item = buildReadableItem(target, libraryMap, folderMap, documentMap, + readablePermissionIds); + if (item != null) { + items.add(item); + } + } + return items; + } + + /** + * 将互动目标转换为当前用户可读的展示对象 + * + * @param target 互动目标 + * @param libraryMap 知识库 Map + * @param folderMap 文件夹 Map + * @param documentMap 文档 Map + * @param readablePermissionIds 可读内容权限编号集合 + * @return 可读的互动展示对象;不可读时返回 {@code null} + */ + private PmsKnowledgeInteractionItemRespVO buildReadableItem( + PmsKnowledgeInteractionItemRespVO target, Map libraryMap, + Map folderMap, Map documentMap, + Set readablePermissionIds) { + // 1. 仅处理仍处于正常状态的所属知识库 + PmsKnowledgeLibraryDO library = libraryMap.get(target.getLibraryId()); + if (library == null || ObjectUtil.notEqual( + PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus(), library.getStatus())) { + return null; + } + // 2. 组装三类对象共用的互动展示字段 + PmsKnowledgeInteractionItemRespVO item = new PmsKnowledgeInteractionItemRespVO() + .setId(target.getId()).setType(target.getType()).setEntityId(target.getEntityId()) + .setLibraryId(target.getLibraryId()).setLibraryName(library.getName()) + .setCreateTime(target.getCreateTime()); + // 3. 知识库对象必须与互动记录的知识库编号一致 + if (PmsKnowledgeObjectTypeEnum.LIBRARY.getType().equals(target.getType())) { + return target.getEntityId().equals(target.getLibraryId()) + ? item.setName(library.getName()).setDescription(library.getDescription()) + .setTargetUpdateTime(library.getUpdateTime()) : null; + } + // 4. 文件夹对象必须归属当前知识库、状态正常且具有内容读取权限 + if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(target.getType())) { + PmsKnowledgeFolderDO folder = folderMap.get(target.getEntityId()); + return folder != null && target.getLibraryId().equals(folder.getLibraryId()) + && PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus().equals(folder.getStatus()) + && readablePermissionIds.contains(folder.getPermissionId()) + ? item.setName(folder.getTitle()).setFolderId(folder.getId()) + .setTargetUpdateTime(folder.getUpdateTime()) : null; + } + // 5. 文档对象必须归属当前知识库、类型匹配、未回收且具有内容读取权限 + PmsKnowledgeDocumentDO document = documentMap.get(target.getEntityId()); + if (document == null || !target.getLibraryId().equals(document.getLibraryId()) + || PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus().equals(document.getStatus()) + || !target.getType().equals(document.getType()) + || !readablePermissionIds.contains(document.getPermissionId())) { + return null; + } + return item.setName(document.getTitle()).setFolderId(document.getFolderId()) + .setDocumentId(document.getId()).setFileType(document.getFileType()).setFileSize(document.getFileSize()) + .setTargetUpdateTime(document.getUpdateTime()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordService.java new file mode 100644 index 000000000..c46e7fd1d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordService.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.viewrecord.PmsKnowledgeRecentListRespVO; + +import java.util.Collection; + +/** + * PMS 知识浏览记录 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeViewRecordService { + + /** + * 记录对象的最近浏览时间 + * + * @param libraryId 知识库编号 + * @param type 对象类型 + * @param entityId 对象编号 + * @param userId 用户编号 + */ + void createViewRecord(Long libraryId, Integer type, Long entityId, Long userId); + + /** + * 获得最近浏览列表 + * + * @param libraryId 知识库编号,为空时查询全部可读知识库 + * @param userId 用户编号 + * @return 最近浏览列表 + */ + PmsKnowledgeRecentListRespVO getRecentViewRecordList(Long libraryId, Long userId); + + /** + * 删除知识库的浏览记录 + * + * @param libraryId 知识库编号 + */ + void deleteViewRecordsByLibraryId(Long libraryId); + + /** + * 删除文件夹和文档的浏览记录 + * + * @param folderIds 文件夹编号集合 + * @param documentIds 文档编号集合 + */ + void deleteViewRecordsByEntityIds(Collection folderIds, Collection documentIds); + + /** + * 更新文件夹和文档浏览记录的所属知识库 + * + * @param folderIds 文件夹编号集合 + * @param documentIds 文档编号集合 + * @param libraryId 目标知识库编号 + */ + void updateViewRecordLibraryIdByEntityIds(Collection folderIds, Collection documentIds, + Long libraryId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordServiceImpl.java new file mode 100644 index 000000000..e390a84fb --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordServiceImpl.java @@ -0,0 +1,146 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.viewrecord.PmsKnowledgeRecentListRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeViewRecordDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeViewRecordMapper; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * PMS 知识浏览记录 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeViewRecordServiceImpl implements PmsKnowledgeViewRecordService { + + /** + * 今天、昨天之外保留的历史自然日数量 + */ + private static final long RECENT_HISTORY_DAYS = 31L; + + @Resource + private PmsKnowledgeViewRecordMapper viewRecordMapper; + + @Resource + private PmsKnowledgeInteractionTargetService interactionTargetService; + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createViewRecord(Long libraryId, Integer type, Long entityId, Long userId) { + // 1. 已浏览过同一对象时,更新当前归属和最后访问时间 + LocalDateTime viewTime = LocalDateTime.now(); + if (viewRecordMapper.updateLibraryIdAndCreateTimeByUserIdAndTypeAndEntityId( + libraryId, viewTime, userId, type, entityId) > 0) { + return; + } + + // 2. 首次浏览时创建最近访问记录 + PmsKnowledgeViewRecordDO record = new PmsKnowledgeViewRecordDO() + .setLibraryId(libraryId).setType(type) + .setEntityId(entityId).setUserId(userId); + record.setCreateTime(viewTime); + viewRecordMapper.insert(record); + } + + @Override + public PmsKnowledgeRecentListRespVO getRecentViewRecordList(Long libraryId, Long userId) { + // 1. 校验指定知识库的读取权限 + if (libraryId != null) { + libraryMemberService.validateLibraryReadable(libraryId, userId); + } + + // 2. 查询今天、昨天及此前 30 个自然日的浏览记录(从当天零点回溯 31 天) + LocalDateTime todayBeginTime = LocalDate.now().atStartOfDay(); + LocalDateTime yesterdayBeginTime = todayBeginTime.minusDays(1); + List records = viewRecordMapper.selectListByUserIdAndCreateTimeAfter( + userId, todayBeginTime.minusDays(RECENT_HISTORY_DAYS)); + if (libraryId != null) { + records.removeIf(record -> ObjectUtil.notEqual(libraryId, record.getLibraryId())); + } + List items = interactionTargetService.getReadableItemList( + convertList(records, record -> new PmsKnowledgeInteractionItemRespVO().setId(record.getId()) + .setType(record.getType()).setEntityId(record.getEntityId()) + .setLibraryId(record.getLibraryId()).setCreateTime(record.getCreateTime())), userId); + + // 3. 按今天、昨天和此前 30 天分组,同一分组内每个对象只保留最近一次访问 + List todayItems = new ArrayList<>(); + List yesterdayItems = new ArrayList<>(); + List recent30DayItems = new ArrayList<>(); + Set todayKeys = new LinkedHashSet<>(); + Set yesterdayKeys = new LinkedHashSet<>(); + Set recentKeys = new LinkedHashSet<>(); + for (PmsKnowledgeInteractionItemRespVO item : items) { + String key = item.getType() + ":" + item.getEntityId(); + if (!item.getCreateTime().isBefore(todayBeginTime)) { + addRecentItem(todayItems, todayKeys, key, item); + } else if (!item.getCreateTime().isBefore(yesterdayBeginTime)) { + addRecentItem(yesterdayItems, yesterdayKeys, key, item); + } else { + addRecentItem(recent30DayItems, recentKeys, key, item); + } + } + return new PmsKnowledgeRecentListRespVO().setTodayItems(todayItems).setYesterdayItems(yesterdayItems) + .setRecent30DayItems(recent30DayItems); + } + + @Override + public void deleteViewRecordsByLibraryId(Long libraryId) { + viewRecordMapper.deleteByLibraryId(libraryId); + } + + @Override + public void deleteViewRecordsByEntityIds(Collection folderIds, Collection documentIds) { + if (CollUtil.isNotEmpty(folderIds)) { + viewRecordMapper.deleteByTypeAndEntityIds(Collections.singleton( + PmsKnowledgeObjectTypeEnum.FOLDER.getType()), folderIds); + } + if (CollUtil.isNotEmpty(documentIds)) { + viewRecordMapper.deleteByTypeAndEntityIds(Arrays.asList(PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), + PmsKnowledgeObjectTypeEnum.FILE.getType()), documentIds); + } + } + + @Override + public void updateViewRecordLibraryIdByEntityIds(Collection folderIds, Collection documentIds, + Long libraryId) { + if (CollUtil.isNotEmpty(folderIds)) { + viewRecordMapper.updateLibraryIdByTypeAndEntityIds(libraryId, + Collections.singleton(PmsKnowledgeObjectTypeEnum.FOLDER.getType()), folderIds); + } + if (CollUtil.isNotEmpty(documentIds)) { + viewRecordMapper.updateLibraryIdByTypeAndEntityIds(libraryId, + Arrays.asList(PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), PmsKnowledgeObjectTypeEnum.FILE.getType()), documentIds); + } + } + + private void addRecentItem(List items, Set keys, + String key, PmsKnowledgeInteractionItemRespVO item) { + if (keys.add(key)) { + items.add(item); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupService.java new file mode 100644 index 000000000..dea97b3fd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupService.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeGroupSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeGroupSortReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeLibraryMoveGroupReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeGroupDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * PMS 知识库分组 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeGroupService { + + /** + * 创建知识库分组,初始化默认分组并追加到现有分组末尾 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @return 分组编号 + */ + Long createGroup(PmsKnowledgeGroupSaveReqVO saveReqVO, Long userId); + + /** + * 更新知识库分组名称,默认分组不允许修改 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + */ + void updateGroup(PmsKnowledgeGroupSaveReqVO saveReqVO, Long userId); + + /** + * 更新知识库分组显示顺序 + * + * @param sortReqVO 排序信息 + * @param userId 用户编号 + */ + void updateGroupSort(PmsKnowledgeGroupSortReqVO sortReqVO, Long userId); + + /** + * 删除知识库分组及其知识库关系,默认分组不允许删除 + * + * @param id 分组编号 + * @param userId 用户编号 + */ + void deleteGroup(Long id, Long userId); + + /** + * 获得当前用户的知识库分组列表,并统计各分组的知识库数量 + * + * @param userId 用户编号 + * @return 分组列表 + */ + List getGroupList(Long userId); + + /** + * 统计当前用户各分组中的可读知识库数量 + * + * @param userId 用户编号 + * @param groupIds 用户分组编号集合 + * @return 分组编号到知识库数量 + */ + Map getGroupLibraryCountMap(Long userId, Collection groupIds); + + /** + * 获得当前用户的知识库分组 + * + * @param id 分组编号 + * @param userId 用户编号 + * @return 知识库分组 + */ + PmsKnowledgeGroupDO getGroup(Long id, Long userId); + + /** + * 移动知识库到个人分组,目标分组为空或“未分组”时移出分组 + * + * @param moveReqVO 移动信息 + * @param userId 用户编号 + */ + void moveLibraryToGroup(PmsKnowledgeLibraryMoveGroupReqVO moveReqVO, Long userId); + + /** + * 按分组过滤知识库编号列表,“全部知识库”不额外过滤,“未分组”排除已分组知识库 + * + * @param groupId 分组编号 + * @param userId 用户编号 + * @param libraryIds 待过滤的知识库编号集合 + * @return 过滤后的知识库编号列表 + */ + List filterLibraryIdListByGroup(Long groupId, Long userId, Collection libraryIds); + + /** + * 删除知识库的全部个人分组关系 + * + * @param libraryId 知识库编号 + */ + void deleteKnowledgeGroupRelationsByLibraryId(Long libraryId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupServiceImpl.java new file mode 100644 index 000000000..25252dbde --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupServiceImpl.java @@ -0,0 +1,277 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeGroupSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeGroupSortReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeLibraryMoveGroupReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeGroupDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeGroupRelationDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeGroupMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeGroupRelationMapper; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeGroupTypeEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.hutool.core.util.ObjectUtil.notEqual; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_GROUP_DEFAULT_CANNOT_DELETE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_GROUP_DEFAULT_CANNOT_MODIFY; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_GROUP_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_GROUP_NOT_EXISTS; + +/** + * PMS 知识库分组 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeGroupServiceImpl implements PmsKnowledgeGroupService { + + @Resource + private PmsKnowledgeGroupMapper groupMapper; + @Resource + private PmsKnowledgeGroupRelationMapper groupRelationMapper; + + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + + @Override + public Long createGroup(PmsKnowledgeGroupSaveReqVO saveReqVO, Long userId) { + // 1. 校验分组名称唯一 + String name = StrUtil.trim(saveReqVO.getName()); + validateKnowledgeGroupNameDuplicate(userId, name, null); + + // 2. 创建自定义分组,并追加到现有分组末尾 + PmsKnowledgeGroupDO lastGroup = groupMapper.selectLastByUserId(userId); + int sort = lastGroup == null || lastGroup.getSort() == null + ? 2 : Math.max(lastGroup.getSort() + 1, 2); + PmsKnowledgeGroupDO group = BeanUtils.toBean(saveReqVO, PmsKnowledgeGroupDO.class) + .setUserId(userId).setName(name) + .setSort(sort) + .setType(PmsKnowledgeGroupTypeEnum.CUSTOM.getType()); + groupMapper.insert(group); + return group.getId(); + } + + @Override + public void updateGroup(PmsKnowledgeGroupSaveReqVO saveReqVO, Long userId) { + // 1.1 校验分组存在 + PmsKnowledgeGroupDO group = validateKnowledgeGroupExists(saveReqVO.getId(), userId); + // 1.2 校验分组为自定义分组 + if (notEqual(PmsKnowledgeGroupTypeEnum.CUSTOM.getType(), group.getType())) { + throw exception(KNOWLEDGE_GROUP_DEFAULT_CANNOT_MODIFY); + } + // 1.3 校验分组名称唯一 + validateKnowledgeGroupNameDuplicate(userId, StrUtil.trim(saveReqVO.getName()), saveReqVO.getId()); + + // 2. 更新分组名称 + groupMapper.updateById(BeanUtils.toBean(saveReqVO, PmsKnowledgeGroupDO.class) + .setName(StrUtil.trim(saveReqVO.getName())).setUserId(null).setSort(null).setType(null)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateGroupSort(PmsKnowledgeGroupSortReqVO sortReqVO, Long userId) { + // 1. 校验排序列表中的分组都属于当前用户 + List groupIds = convertList(sortReqVO.getItems(), PmsKnowledgeGroupSortReqVO.Item::getId); + if (groupMapper.selectListByIdsAndUserId(groupIds, userId).size() != groupIds.size()) { + throw exception(KNOWLEDGE_GROUP_NOT_EXISTS); + } + + // 2. 更新全部分组的显示顺序 + groupMapper.updateBatch(convertList(sortReqVO.getItems(), + item -> new PmsKnowledgeGroupDO().setId(item.getId()).setSort(item.getSort()))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteGroup(Long id, Long userId) { + // 1.1 校验分组存在 + PmsKnowledgeGroupDO group = validateKnowledgeGroupExists(id, userId); + // 1.2 校验分组为自定义分组 + if (notEqual(PmsKnowledgeGroupTypeEnum.CUSTOM.getType(), group.getType())) { + throw exception(KNOWLEDGE_GROUP_DEFAULT_CANNOT_DELETE); + } + + // 2. 删除分组及其知识库关系 + groupRelationMapper.deleteByUserIdAndGroupId(userId, id); + groupMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getGroupList(Long userId) { + // 1. 查询当前用户的知识库分组 + List groups = groupMapper.selectListByUserId(userId); + // 2. 仅在默认分组缺失时补齐,避免正常访问产生写操作 + ensureDefaultKnowledgeGroupList(userId, groups); + return groups; + } + + @Override + public Map getGroupLibraryCountMap(Long userId, Collection groupIds) { + if (CollUtil.isEmpty(groupIds)) { + return Collections.emptyMap(); + } + // 1. 只统计当前用户可读的知识库,公开库也应进入“全部知识库”数量 + List readableLibraryIds = libraryMemberService.getReadableLibraryIdList(userId); + Map groupCountMap = CollUtil.isEmpty(readableLibraryIds) + ? new LinkedHashMap<>() + : groupRelationMapper.selectGroupLibraryCountMapByUserIdAndLibraryIds(userId, readableLibraryIds); + int groupedLibraryCount = CollUtil.isEmpty(readableLibraryIds) ? 0 + : groupRelationMapper.selectGroupedLibraryCountByUserIdAndLibraryIds(userId, readableLibraryIds); + + // 2. 补充系统分组数量:全部=可读总数,未分组=可读总数-已分组数 + int readableLibraryCount = CollUtil.size(readableLibraryIds); + List groups = groupMapper.selectByIds(groupIds); + for (PmsKnowledgeGroupDO group : groups) { + if (PmsKnowledgeGroupTypeEnum.ALL.getType().equals(group.getType())) { + groupCountMap.put(group.getId(), readableLibraryCount); + } else if (PmsKnowledgeGroupTypeEnum.UNGROUPED.getType().equals(group.getType())) { + groupCountMap.put(group.getId(), Math.max(readableLibraryCount - groupedLibraryCount, 0)); + } + } + return groupCountMap; + } + + @Override + public PmsKnowledgeGroupDO getGroup(Long id, Long userId) { + return validateKnowledgeGroupExists(id, userId); + } + + /** + * 确保用户拥有“全部知识库”和“未分组”两个系统分组 + * + * @param userId 用户编号 + * @param groups 当前已查询到的分组 + */ + private void ensureDefaultKnowledgeGroupList(Long userId, List groups) { + // 1. 检查两个系统分组是否已经存在 + boolean hasAllGroup = CollUtil.findOne(groups, + group -> PmsKnowledgeGroupTypeEnum.ALL.getType().equals(group.getType())) != null; + boolean hasUngroupedGroup = CollUtil.findOne(groups, + group -> PmsKnowledgeGroupTypeEnum.UNGROUPED.getType().equals(group.getType())) != null; + if (hasAllGroup && hasUngroupedGroup) { + return; + } + + // 2. 构造并插入缺失的系统分组 + List defaultGroups = new ArrayList<>(); + if (!hasAllGroup) { + defaultGroups.add(new PmsKnowledgeGroupDO().setUserId(userId) + .setName(PmsKnowledgeGroupTypeEnum.ALL.getName()).setSort(0) + .setType(PmsKnowledgeGroupTypeEnum.ALL.getType())); + } + if (!hasUngroupedGroup) { + defaultGroups.add(new PmsKnowledgeGroupDO().setUserId(userId) + .setName(PmsKnowledgeGroupTypeEnum.UNGROUPED.getName()).setSort(1) + .setType(PmsKnowledgeGroupTypeEnum.UNGROUPED.getType())); + } + groupMapper.insertBatch(defaultGroups); + + // 3. 直接合并本次新增记录并保持排序 + groups.addAll(defaultGroups); + groups.sort(Comparator.comparing(PmsKnowledgeGroupDO::getSort, + Comparator.nullsLast(Integer::compareTo)).thenComparing(PmsKnowledgeGroupDO::getId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void moveLibraryToGroup(PmsKnowledgeLibraryMoveGroupReqVO moveReqVO, Long userId) { + // 1. 校验当前用户可以读取知识库;个人分组允许收藏公开知识库 + libraryMemberService.validateLibraryReadable(moveReqVO.getLibraryId(), userId); + + // 2. 移出分组时,删除已有的个人分组关系 + if (moveReqVO.getGroupId() == null) { + groupRelationMapper.deleteByUserIdAndLibraryId(userId, moveReqVO.getLibraryId()); + return; + } + + // 3. 校验目标分组;移动到“未分组”等同于移出分组,“全部知识库”不能作为目标分组 + PmsKnowledgeGroupDO targetGroup = validateKnowledgeGroupExists(moveReqVO.getGroupId(), userId); + if (PmsKnowledgeGroupTypeEnum.UNGROUPED.getType().equals(targetGroup.getType())) { + groupRelationMapper.deleteByUserIdAndLibraryId(userId, moveReqVO.getLibraryId()); + return; + } + if (PmsKnowledgeGroupTypeEnum.ALL.getType().equals(targetGroup.getType())) { + throw exception(KNOWLEDGE_GROUP_DEFAULT_CANNOT_MODIFY); + } + + // 4. 新增或更新个人知识库分组关系 + PmsKnowledgeGroupRelationDO relation = groupRelationMapper.selectByUserIdAndLibraryId( + userId, moveReqVO.getLibraryId()); + if (relation == null) { + groupRelationMapper.insert(BeanUtils.toBean(moveReqVO, PmsKnowledgeGroupRelationDO.class) + .setUserId(userId).setSort(0)); + return; + } + groupRelationMapper.updateById(BeanUtils.toBean(moveReqVO, PmsKnowledgeGroupRelationDO.class) + .setId(relation.getId()).setUserId(null).setLibraryId(null).setSort(null)); + } + + @Override + public List filterLibraryIdListByGroup(Long groupId, Long userId, Collection libraryIds) { + if (CollUtil.isEmpty(libraryIds) || groupId == null) { + return CollUtil.isEmpty(libraryIds) ? Collections.emptyList() : new ArrayList<>(new HashSet<>(libraryIds)); + } + + // 1. 校验分组存在;“全部知识库”不额外过滤 + PmsKnowledgeGroupDO group = validateKnowledgeGroupExists(groupId, userId); + if (PmsKnowledgeGroupTypeEnum.ALL.getType().equals(group.getType())) { + return new ArrayList<>(new HashSet<>(libraryIds)); + } + + // 2. 获得个人分组中的知识库编号 + Set candidateLibraryIds = new HashSet<>(libraryIds); + if (PmsKnowledgeGroupTypeEnum.UNGROUPED.getType().equals(group.getType())) { + Set groupedLibraryIds = convertSet(groupRelationMapper.selectListByUserIdAndLibraryIds( + userId, candidateLibraryIds), PmsKnowledgeGroupRelationDO::getLibraryId); + candidateLibraryIds.removeAll(groupedLibraryIds); + return new ArrayList<>(candidateLibraryIds); + } + // 自定义分组只保留已建立当前分组关系的候选知识库 + List relations = + groupRelationMapper.selectListByUserIdAndGroupId(userId, groupId); + Set groupedLibraryIds = convertSet(relations, PmsKnowledgeGroupRelationDO::getLibraryId); + candidateLibraryIds.retainAll(groupedLibraryIds); + return new ArrayList<>(candidateLibraryIds); + } + + @Override + public void deleteKnowledgeGroupRelationsByLibraryId(Long libraryId) { + groupRelationMapper.deleteByLibraryId(libraryId); + } + + private PmsKnowledgeGroupDO validateKnowledgeGroupExists(Long id, Long userId) { + PmsKnowledgeGroupDO group = groupMapper.selectByIdAndUserId(id, userId); + if (group == null) { + throw exception(KNOWLEDGE_GROUP_NOT_EXISTS); + } + return group; + } + + private void validateKnowledgeGroupNameDuplicate(Long userId, String name, Long excludeId) { + PmsKnowledgeGroupDO group = groupMapper.selectByUserIdAndName(userId, name, excludeId); + if (group != null) { + throw exception(KNOWLEDGE_GROUP_NAME_DUPLICATE); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberService.java new file mode 100644 index 000000000..98abd99f3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberService.java @@ -0,0 +1,166 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.member.PmsKnowledgeLibraryUpdateMemberListReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * PMS 知识库成员 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeLibraryMemberService { + + /** + * 校验知识库可读 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 知识库 + */ + PmsKnowledgeLibraryDO validateLibraryReadable(Long libraryId, Long userId); + + /** + * 校验知识库可写 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 知识库 + */ + PmsKnowledgeLibraryDO validateLibraryWritable(Long libraryId, Long userId); + + /** + * 校验知识库管理员权限 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 知识库 + */ + PmsKnowledgeLibraryDO validateLibraryAdmin(Long libraryId, Long userId); + + /** + * 校验知识库创建人权限 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 知识库 + */ + PmsKnowledgeLibraryDO validateLibraryCreator(Long libraryId, Long userId); + + /** + * 判断知识库是否可写 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 是否可写 + */ + boolean isLibraryWritable(Long libraryId, Long userId); + + /** + * 判断是否为知识库管理员 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 是否为管理员 + */ + boolean isLibraryAdmin(Long libraryId, Long userId); + + /** + * 获得已加入的有效知识库编号列表 + * + * @param userId 用户编号 + * @return 知识库编号列表 + */ + List getJoinedLibraryIdList(Long userId); + + /** + * 获得可读知识库编号列表 + * + * @param userId 用户编号 + * @return 知识库编号列表 + */ + List getReadableLibraryIdList(Long userId); + + /** + * 创建知识库的创建人和初始成员 + * + * @param libraryId 知识库编号 + * @param creatorUserId 创建人用户编号 + * @param adminUserIds 初始管理员用户编号集合 + * @param memberUserIds 初始普通成员用户编号集合 + */ + void createLibraryMemberList(Long libraryId, Long creatorUserId, + Collection adminUserIds, Collection memberUserIds); + + /** + * 获得知识库成员列表 + * + * @param libraryId 知识库编号 + * @param userId 当前用户编号 + * @return 成员列表 + */ + List getLibraryMemberList(Long libraryId, Long userId); + + /** + * 更新知识库成员,保留创建人并清理已移除成员的内容级协作权限 + * + * @param updateReqVO 更新信息 + * @param userId 当前用户编号 + */ + void updateLibraryMemberList(PmsKnowledgeLibraryUpdateMemberListReqVO updateReqVO, Long userId); + + /** + * 退出知识库,创建人不允许退出 + * + * @param libraryId 知识库编号 + * @param userId 当前用户编号 + */ + void exitLibrary(Long libraryId, Long userId); + + /** + * 删除知识库的全部成员关系 + * + * @param libraryId 知识库编号 + */ + void deleteLibraryMembersByLibraryId(Long libraryId); + + /** + * 获得知识库成员列表 Map + * + * @param libraryIds 知识库编号集合 + * @return 知识库编号到成员列表的映射 + */ + Map> getLibraryMemberListMap(Collection libraryIds); + + /** + * 获得用户在知识库中的直接成员关系 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 成员关系 + */ + PmsKnowledgeLibraryMemberDO getMemberByLibraryIdAndUserId(Long libraryId, Long userId); + + /** + * 获得部门在知识库中的成员关系 + * + * @param libraryId 知识库编号 + * @param deptId 部门编号 + * @return 成员关系 + */ + PmsKnowledgeLibraryMemberDO getMemberByLibraryIdAndDeptId(Long libraryId, Long deptId); + + /** + * 获得用户或部门加入的知识库编号列表 + * + * @param userId 用户编号 + * @param deptId 部门编号 + * @return 知识库编号列表 + */ + List getLibraryIdListByUserIdOrDeptId(Long userId, Long deptId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberServiceImpl.java new file mode 100644 index 000000000..9d6788462 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberServiceImpl.java @@ -0,0 +1,337 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.member.PmsKnowledgeLibraryUpdateMemberListReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMemberMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static cn.hutool.core.util.ObjectUtil.notEqual; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.equalsAny; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_ADMIN_REQUIRED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_CREATOR_CANNOT_REMOVE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_DIRECT_MEMBER_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_MEMBERS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_WRITE_ACCESS_DENIED; + +/** + * PMS 知识库成员 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeLibraryMemberServiceImpl implements PmsKnowledgeLibraryMemberService { + + @Resource + private PmsKnowledgeLibraryMemberMapper memberMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeLibraryService libraryService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeContentPermissionService contentPermissionService; + + @Resource + private PermissionApi permissionApi; + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @Override + public PmsKnowledgeLibraryDO validateLibraryReadable(Long libraryId, Long userId) { + PmsKnowledgeLibraryDO library = validateLibraryExists(libraryId); + if (Boolean.FALSE.equals(library.getOpenStatus()) && !isLibraryMember(libraryId, userId)) { + throw exception(KNOWLEDGE_LIBRARY_ACCESS_DENIED); + } + return library; + } + + @Override + public PmsKnowledgeLibraryDO validateLibraryWritable(Long libraryId, Long userId) { + PmsKnowledgeLibraryDO library = validateLibraryExists(libraryId); + if (!isLibraryWritable(libraryId, userId)) { + throw exception(KNOWLEDGE_LIBRARY_WRITE_ACCESS_DENIED); + } + return library; + } + + @Override + public PmsKnowledgeLibraryDO validateLibraryAdmin(Long libraryId, Long userId) { + PmsKnowledgeLibraryDO library = validateLibraryExists(libraryId); + if (!isLibraryAdmin(libraryId, userId)) { + throw exception(KNOWLEDGE_LIBRARY_ADMIN_REQUIRED); + } + return library; + } + + @Override + public PmsKnowledgeLibraryDO validateLibraryCreator(Long libraryId, Long userId) { + PmsKnowledgeLibraryDO library = validateLibraryExists(libraryId); + if (isSuperAdmin(userId)) { + return library; + } + PmsKnowledgeLibraryMemberDO member = getLibraryMember(libraryId, userId); + if (member == null || notEqual(PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel(), member.getLevel())) { + throw exception(KNOWLEDGE_LIBRARY_ADMIN_REQUIRED); + } + return library; + } + + @Override + public boolean isLibraryWritable(Long libraryId, Long userId) { + return isSuperAdmin(userId) || getLibraryMember(libraryId, userId) != null; + } + + @Override + public boolean isLibraryAdmin(Long libraryId, Long userId) { + if (isSuperAdmin(userId)) { + return true; + } + PmsKnowledgeLibraryMemberDO member = getLibraryMember(libraryId, userId); + if (member == null) { + return false; + } + // 公开库也允许管理员参与成员和内容协作;创建人权限单独用于公开状态和删除等高危操作 + return equalsAny(member.getLevel(), PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel(), + PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel()); + } + + @Override + public List getJoinedLibraryIdList(Long userId) { + // 1. 查询当前用户及所在部门加入的知识库编号 + AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); + List joinedLibraryIds = getLibraryIdListByUserIdOrDeptId( + userId, user != null ? user.getDeptId() : null); + + // 2. 过滤已删除或已进入回收站的知识库 + Set libraryIds = new LinkedHashSet<>(joinedLibraryIds); + libraryIds.retainAll(libraryService.getLibraryIdList(null)); + return convertList(libraryIds, item -> item); + } + + @Override + public List getReadableLibraryIdList(Long userId) { + // 1. 超级管理员可读取全部知识库 + if (isSuperAdmin(userId)) { + return libraryService.getLibraryIdList(null); + } + + // 2. 查询公开知识库和自己加入的知识库 + List openLibraryIds = libraryService.getLibraryIdList(true); + List joinedLibraryIds = getJoinedLibraryIdList(userId); + + // 3. 合并并去重可读知识库编号 + Set libraryIds = new LinkedHashSet<>(openLibraryIds); + libraryIds.addAll(joinedLibraryIds); + return convertList(libraryIds, item -> item); + } + + @Override + public void createLibraryMemberList(Long libraryId, Long creatorUserId, + Collection adminUserIds, Collection memberUserIds) { + // 1.1 整理初始成员并校验重复、创建人保护 + List adminUserIdList = CollUtil.isEmpty(adminUserIds) + ? new ArrayList<>() : new ArrayList<>(adminUserIds); + List memberUserIdList = CollUtil.isEmpty(memberUserIds) + ? new ArrayList<>() : new ArrayList<>(memberUserIds); + Set initialMemberUserIds = new LinkedHashSet<>(); + // 两个角色数组统一校验创建人保护和重复用户,避免维护两套不一致的校验分支 + for (Long userId : adminUserIdList) { + if (userId == null || Objects.equals(creatorUserId, userId) + || !initialMemberUserIds.add(userId)) { + throw exception(KNOWLEDGE_LIBRARY_MEMBERS_INVALID); + } + } + for (Long userId : memberUserIdList) { + if (userId == null || Objects.equals(creatorUserId, userId) + || !initialMemberUserIds.add(userId)) { + throw exception(KNOWLEDGE_LIBRARY_MEMBERS_INVALID); + } + } + // 1.2 校验初始成员账号存在 + adminUserApi.validateUserList(initialMemberUserIds).checkError(); + + // 2. 创建创建人和初始成员 + List members = new ArrayList<>(); + members.addAll(convertList(adminUserIdList, userId -> new PmsKnowledgeLibraryMemberDO() + .setLibraryId(libraryId).setUserId(userId) + .setLevel(PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel()))); + members.addAll(convertList(memberUserIdList, userId -> new PmsKnowledgeLibraryMemberDO() + .setLibraryId(libraryId).setUserId(userId) + .setLevel(PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel()))); + members.add(0, new PmsKnowledgeLibraryMemberDO().setLibraryId(libraryId).setUserId(creatorUserId) + .setLevel(PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + memberMapper.insertBatch(members); + } + + @Override + public List getLibraryMemberList(Long libraryId, Long userId) { + validateLibraryReadable(libraryId, userId); + return memberMapper.selectListByLibraryId(libraryId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateLibraryMemberList(PmsKnowledgeLibraryUpdateMemberListReqVO updateReqVO, Long userId) { + // 1.1 校验知识库管理员权限 + validateLibraryAdmin(updateReqVO.getLibraryId(), userId); + // 1.2 校验成员不重复 + Set memberUserIds = convertSet(updateReqVO.getMembers(), + PmsKnowledgeLibraryUpdateMemberListReqVO.Member::getUserId); + Set memberDeptIds = convertSet(updateReqVO.getMembers(), + PmsKnowledgeLibraryUpdateMemberListReqVO.Member::getDeptId); + List oldMembers = + memberMapper.selectListByLibraryId(updateReqVO.getLibraryId()); + PmsKnowledgeLibraryMemberDO creator = CollUtil.findOne(oldMembers, member -> + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel().equals(member.getLevel())); + if (memberUserIds.size() + memberDeptIds.size() != updateReqVO.getMembers().size()) { + throw exception(KNOWLEDGE_LIBRARY_MEMBERS_INVALID); + } + // 1.3 校验成员不包含知识库创建人 + if (creator != null && memberUserIds.contains(creator.getUserId())) { + throw exception(KNOWLEDGE_LIBRARY_MEMBERS_INVALID); + } + // 1.4 校验成员不能伪造创建人等级 + if (CollUtil.findOne(updateReqVO.getMembers(), member -> + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel().equals(member.getLevel())) != null) { + throw exception(KNOWLEDGE_LIBRARY_MEMBERS_INVALID); + } + // 1.5 校验用户成员存在 + adminUserApi.validateUserList(memberUserIds).checkError(); + // 1.6 校验部门成员存在 + deptApi.validateDeptList(memberDeptIds).checkError(); + + // 2. 保留创建人并重建管理员和普通成员 + memberMapper.deleteByLibraryIdAndLevels(updateReqVO.getLibraryId(), Arrays.asList( + PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel(), + PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel())); + if (CollUtil.isNotEmpty(updateReqVO.getMembers())) { + memberMapper.insertBatch(convertList(updateReqVO.getMembers(), member -> + BeanUtils.toBean(member, PmsKnowledgeLibraryMemberDO.class) + .setLibraryId(updateReqVO.getLibraryId()))); + } + + // 3. 移除已退出知识库成员的内容级协作权限 + Set retainedUserIds = new LinkedHashSet<>(memberUserIds); + if (creator != null) { + retainedUserIds.add(creator.getUserId()); + } + Set removedUserIds = convertSet(oldMembers, PmsKnowledgeLibraryMemberDO::getUserId); + removedUserIds.removeAll(retainedUserIds); + Set removedDeptIds = convertSet(oldMembers, PmsKnowledgeLibraryMemberDO::getDeptId); + removedDeptIds.removeAll(memberDeptIds); + contentPermissionService.deleteContentPermissionMembersByLibraryId(updateReqVO.getLibraryId(), removedUserIds, removedDeptIds); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void exitLibrary(Long libraryId, Long userId) { + // 1.1 校验知识库可访问 + validateLibraryReadable(libraryId, userId); + // 1.2 校验当前账号是直接成员 + PmsKnowledgeLibraryMemberDO member = memberMapper.selectByLibraryIdAndUserId(libraryId, userId); + if (member == null) { + throw exception(KNOWLEDGE_LIBRARY_DIRECT_MEMBER_NOT_EXISTS); + } + // 1.3 校验知识库创建人不能主动退出 + if (PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel().equals(member.getLevel())) { + throw exception(KNOWLEDGE_LIBRARY_CREATOR_CANNOT_REMOVE); + } + + // 2. 退出知识库并清理当前账号的内容级协作权限 + memberMapper.deleteByLibraryIdAndUserId(libraryId, userId); + contentPermissionService.deleteContentPermissionMembersByLibraryId(libraryId, Collections.singleton(userId), + Collections.emptySet()); + } + + @Override + public void deleteLibraryMembersByLibraryId(Long libraryId) { + memberMapper.deleteByLibraryId(libraryId); + } + + @Override + public Map> getLibraryMemberListMap(Collection libraryIds) { + if (CollUtil.isEmpty(libraryIds)) { + return Collections.emptyMap(); + } + return convertMultiMap(memberMapper.selectListByLibraryIds(libraryIds), + PmsKnowledgeLibraryMemberDO::getLibraryId); + } + + @Override + public PmsKnowledgeLibraryMemberDO getMemberByLibraryIdAndUserId(Long libraryId, Long userId) { + return memberMapper.selectByLibraryIdAndUserId(libraryId, userId); + } + + @Override + public PmsKnowledgeLibraryMemberDO getMemberByLibraryIdAndDeptId(Long libraryId, Long deptId) { + return memberMapper.selectByLibraryIdAndDeptId(libraryId, deptId); + } + + @Override + public List getLibraryIdListByUserIdOrDeptId(Long userId, Long deptId) { + return memberMapper.selectLibraryIdListByUserIdOrDeptId(userId, deptId); + } + + private PmsKnowledgeLibraryDO validateLibraryExists(Long libraryId) { + PmsKnowledgeLibraryDO library = libraryService.getLibrary(libraryId); + if (library == null || notEqual(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus(), library.getStatus())) { + throw exception(KNOWLEDGE_LIBRARY_NOT_EXISTS); + } + return library; + } + + private boolean isSuperAdmin(Long userId) { + return permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData(); + } + + private boolean isLibraryMember(Long libraryId, Long userId) { + return isSuperAdmin(userId) || getLibraryMember(libraryId, userId) != null; + } + + private PmsKnowledgeLibraryMemberDO getLibraryMember(Long libraryId, Long userId) { + PmsKnowledgeLibraryMemberDO member = getMemberByLibraryIdAndUserId(libraryId, userId); + if (member != null) { + return member; + } + AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); + if (user == null || user.getDeptId() == null) { + return null; + } + return getMemberByLibraryIdAndDeptId(libraryId, user.getDeptId()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryService.java new file mode 100644 index 000000000..e6e86e5dd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryService.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibraryPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibrarySaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * PMS 知识库 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeLibraryService { + + /** + * 创建知识库,并将创建人和初始成员加入知识库 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @return 知识库编号 + */ + Long createLibrary(PmsKnowledgeLibrarySaveReqVO saveReqVO, Long userId); + + /** + * 更新知识库基本信息 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + */ + void updateLibrary(PmsKnowledgeLibrarySaveReqVO saveReqVO, Long userId); + + /** + * 删除知识库,并将知识库及其内容移入回收站 + * + * @param id 知识库编号 + * @param userId 用户编号 + */ + void deleteLibrary(Long id, Long userId); + + /** + * 将知识库更新为已回收状态 + * + * @param id 知识库编号 + * @param userId 删除用户编号 + * @param deleteTime 删除时间 + */ + void updateLibraryToRecycled(Long id, Long userId, LocalDateTime deleteTime); + + /** + * 恢复知识库 + * + * @param id 知识库编号 + */ + void restoreLibrary(Long id); + + /** + * 彻底删除知识库 + * + * @param id 知识库编号 + */ + void deleteLibraryPermanently(Long id); + + /** + * 获得知识库 + * + * @param id 知识库编号 + * @return 知识库 + */ + PmsKnowledgeLibraryDO getLibrary(Long id); + + /** + * 获得当前用户可见的知识库分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 知识库分页 + */ + PageResult getLibraryPage(PmsKnowledgeLibraryPageReqVO pageReqVO, Long userId); + + /** + * 获得知识库列表 + * + * @param ids 知识库编号集合 + * @return 知识库列表 + */ + List getLibraryList(Collection ids); + + /** + * 获得知识库 Map + * + * @param ids 知识库编号集合 + * @return 知识库编号到知识库的映射 + */ + default Map getLibraryMap(Collection ids) { + return convertMap(getLibraryList(ids), PmsKnowledgeLibraryDO::getId); + } + + /** + * 获得符合公开状态的正常知识库编号列表 + * + * @param openStatus 公开状态,为空时查询全部 + * @return 知识库编号列表 + */ + List getLibraryIdList(Boolean openStatus); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryServiceImpl.java new file mode 100644 index 000000000..24d2b13fd --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryServiceImpl.java @@ -0,0 +1,153 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibraryPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibrarySaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.service.kb.recycle.PmsKnowledgeRecycleService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + + +/** + * PMS 知识库 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeLibraryServiceImpl implements PmsKnowledgeLibraryService { + + @Resource + private PmsKnowledgeLibraryMapper libraryMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeLibraryMemberService memberService; + @Resource + private PmsKnowledgeGroupService knowledgeGroupService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeRecycleService recycleService; + @Resource + private PmsKnowledgeLibraryTemplateService libraryTemplateService; + + @Resource + private PermissionApi permissionApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createLibrary(PmsKnowledgeLibrarySaveReqVO saveReqVO, Long userId) { + // 1. 创建知识库 + PmsKnowledgeLibraryDO library = BeanUtils.toBean(saveReqVO, PmsKnowledgeLibraryDO.class) + .setStatus(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + libraryMapper.insert(library); + + // 2. 创建人和初始成员加入知识库 + memberService.createLibraryMemberList(library.getId(), userId, + saveReqVO.getAdminUserIds(), saveReqVO.getMemberUserIds()); + + // 3. 使用所选模板创建知识库文档 + if (saveReqVO.getTemplateId() != null) { + libraryTemplateService.createTemplateDocumentList(saveReqVO.getTemplateId(), library.getId(), userId); + } + return library.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateLibrary(PmsKnowledgeLibrarySaveReqVO saveReqVO, Long userId) { + // 1.1 普通库设置由管理员维护 + PmsKnowledgeLibraryDO library = memberService.validateLibraryAdmin(saveReqVO.getId(), userId); + // 1.2 公开库转私有仅限创建人 + if (Boolean.TRUE.equals(library.getOpenStatus()) + && Boolean.FALSE.equals(saveReqVO.getOpenStatus())) { + memberService.validateLibraryCreator(library.getId(), userId); + } + + // 2. 更新知识库基本信息 + libraryMapper.updateForEdit(BeanUtils.toBean(saveReqVO, PmsKnowledgeLibraryDO.class)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteLibrary(Long id, Long userId) { + // 1. 校验当前用户是知识库创建人 + PmsKnowledgeLibraryDO library = memberService.validateLibraryCreator(id, userId); + + // 2. 将知识库及其内容移入回收站 + recycleService.recycleLibrary(library, userId); + } + + @Override + public void updateLibraryToRecycled(Long id, Long userId, LocalDateTime deleteTime) { + libraryMapper.updateById(new PmsKnowledgeLibraryDO().setId(id) + .setStatus(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus()) + .setDeleteUserId(userId).setDeleteTime(deleteTime)); + } + + @Override + public void restoreLibrary(Long id) { + libraryMapper.updateToRestoreById(id, PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + } + + @Override + public void deleteLibraryPermanently(Long id) { + libraryMapper.deleteById(id); + } + + @Override + public PmsKnowledgeLibraryDO getLibrary(Long id) { + return libraryMapper.selectById(id); + } + + @Override + public PageResult getLibraryPage(PmsKnowledgeLibraryPageReqVO pageReqVO, Long userId) { + // 1. 获得当前用户参与及可读的知识库,并按个人分组进一步过滤 + List memberLibraryIds = memberService.getJoinedLibraryIdList(userId); + List filterLibraryIds = null; + if (pageReqVO.getGroupId() != null) { + List readableLibraryIds = memberService.getReadableLibraryIdList(userId); + filterLibraryIds = knowledgeGroupService.filterLibraryIdListByGroup( + pageReqVO.getGroupId(), userId, readableLibraryIds); + if (CollUtil.isEmpty(filterLibraryIds)) { + return PageResult.empty(); + } + } + // 2. 超级管理员查询全部知识库,普通用户查询公开或已加入知识库 + boolean includeAll = permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData(); + + // 3. 执行分页查询 + return libraryMapper.selectPage(pageReqVO, memberLibraryIds, includeAll, filterLibraryIds, + PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + } + + @Override + public List getLibraryList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return libraryMapper.selectByIds(ids); + } + + @Override + public List getLibraryIdList(Boolean openStatus) { + return libraryMapper.selectIdList(openStatus, PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateService.java new file mode 100644 index 000000000..4aca58d6d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateService.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplatePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplateSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryTemplateDO; +import org.springframework.lang.Nullable; + +import java.util.List; + +/** + * PMS 知识库模板 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeLibraryTemplateService { + + /** + * 创建知识库模板 + * + * @param createReqVO 创建信息 + * @return 模板编号 + */ + Long createLibraryTemplate(PmsKnowledgeLibraryTemplateSaveReqVO createReqVO); + + /** + * 更新知识库模板 + * + * @param updateReqVO 更新信息 + */ + void updateLibraryTemplate(PmsKnowledgeLibraryTemplateSaveReqVO updateReqVO); + + /** + * 删除知识库模板 + * + * @param id 模板编号 + */ + void deleteLibraryTemplate(Long id); + + /** + * 获得知识库模板 + * + * @param id 模板编号 + * @return 知识库模板 + */ + PmsKnowledgeLibraryTemplateDO getLibraryTemplate(Long id); + + /** + * 获得知识库模板分页 + * + * @param pageReqVO 分页查询 + * @return 知识库模板分页 + */ + PageResult getLibraryTemplatePage( + PmsKnowledgeLibraryTemplatePageReqVO pageReqVO); + + /** + * 获得知识库模板列表 + * + * @param status 模板状态,为空时返回全部状态 + * @return 知识库模板列表 + */ + List getLibraryTemplateList(@Nullable Integer status); + + /** + * 根据知识库模板创建知识库文档 + * + * @param templateId 模板编号 + * @param libraryId 知识库编号 + * @param userId 当前用户编号 + */ + void createTemplateDocumentList(Long templateId, Long libraryId, Long userId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateServiceImpl.java new file mode 100644 index 000000000..1c3e1ef71 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateServiceImpl.java @@ -0,0 +1,158 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplatePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplateSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryTemplateDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryTemplateMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_TEMPLATE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_TEMPLATE_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_TEMPLATE_NOT_EXISTS; + +/** + * PMS 知识库模板 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeLibraryTemplateServiceImpl implements PmsKnowledgeLibraryTemplateService { + + @Resource + private PmsKnowledgeLibraryTemplateMapper libraryTemplateMapper; + @Resource + private PmsKnowledgeDocumentService documentService; + + @Override + public Long createLibraryTemplate(PmsKnowledgeLibraryTemplateSaveReqVO createReqVO) { + // 1. 校验模板名称和文档配置 + validateLibraryTemplateNameDuplicate(null, createReqVO.getName()); + validateLibraryTemplateDocuments(createReqVO.getDocuments()); + + // 2. 创建知识库模板 + PmsKnowledgeLibraryTemplateDO template = BeanUtils.toBean(createReqVO, PmsKnowledgeLibraryTemplateDO.class) + .setDocuments(BeanUtils.toBean(createReqVO.getDocuments(), PmsKnowledgeLibraryTemplateDO.Document.class)); + libraryTemplateMapper.insert(template); + return template.getId(); + } + + @Override + public void updateLibraryTemplate(PmsKnowledgeLibraryTemplateSaveReqVO updateReqVO) { + // 1. 校验模板存在、名称和文档配置 + validateLibraryTemplateExists(updateReqVO.getId()); + validateLibraryTemplateNameDuplicate(updateReqVO.getId(), updateReqVO.getName()); + validateLibraryTemplateDocuments(updateReqVO.getDocuments()); + + // 2. 更新知识库模板 + libraryTemplateMapper.updateById(BeanUtils.toBean(updateReqVO, PmsKnowledgeLibraryTemplateDO.class) + .setDocuments(BeanUtils.toBean(updateReqVO.getDocuments(), PmsKnowledgeLibraryTemplateDO.Document.class))); + } + + @Override + public void deleteLibraryTemplate(Long id) { + // 1. 校验模板存在 + validateLibraryTemplateExists(id); + + // 2. 删除知识库模板 + libraryTemplateMapper.deleteById(id); + } + + @Override + public PmsKnowledgeLibraryTemplateDO getLibraryTemplate(Long id) { + return validateLibraryTemplateExists(id); + } + + @Override + public PageResult getLibraryTemplatePage( + PmsKnowledgeLibraryTemplatePageReqVO pageReqVO) { + return libraryTemplateMapper.selectPage(pageReqVO); + } + + @Override + public List getLibraryTemplateList(Integer status) { + return libraryTemplateMapper.selectListByStatus(status); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void createTemplateDocumentList(Long templateId, Long libraryId, Long userId) { + // 1. 校验模板存在且处于启用状态 + PmsKnowledgeLibraryTemplateDO template = validateLibraryTemplateExists(templateId); + if (CommonStatusEnum.isDisable(template.getStatus())) { + throw exception(KNOWLEDGE_LIBRARY_TEMPLATE_NOT_EXISTS); + } + if (CollUtil.isEmpty(template.getDocuments())) { + throw exception(KNOWLEDGE_LIBRARY_TEMPLATE_INVALID); + } + + // 2. 在知识库根目录创建模板富文本文档;每篇文档独立创建内容权限 + for (PmsKnowledgeLibraryTemplateDO.Document document : template.getDocuments()) { + documentService.createDocument(new PmsKnowledgeDocumentCreateReqVO().setLibraryId(libraryId) + .setFolderId(PmsKnowledgeDocumentDO.FOLDER_ID_ROOT) + .setParentId(PmsKnowledgeDocumentDO.PARENT_ID_ROOT).setTitle(document.getTitle()) + .setType(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType()) + .setContent(document.getContent()), userId); + } + } + + private PmsKnowledgeLibraryTemplateDO validateLibraryTemplateExists(Long id) { + PmsKnowledgeLibraryTemplateDO template = libraryTemplateMapper.selectById(id); + if (template == null) { + throw exception(KNOWLEDGE_LIBRARY_TEMPLATE_NOT_EXISTS); + } + return template; + } + + /** + * 校验知识库模板名称唯一 + * + * @param id 模板编号 + * @param name 模板名称 + */ + private void validateLibraryTemplateNameDuplicate(Long id, String name) { + PmsKnowledgeLibraryTemplateDO template = libraryTemplateMapper.selectByName(name); + if (template == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的模板 + if (id == null) { + throw exception(KNOWLEDGE_LIBRARY_TEMPLATE_NAME_DUPLICATE); + } + if (ObjectUtil.notEqual(template.getId(), id)) { + throw exception(KNOWLEDGE_LIBRARY_TEMPLATE_NAME_DUPLICATE); + } + } + + private void validateLibraryTemplateDocuments(List documents) { + if (CollUtil.isEmpty(documents)) { + throw exception(KNOWLEDGE_LIBRARY_TEMPLATE_INVALID); + } + Set titles = new HashSet<>(); + for (PmsKnowledgeLibraryTemplateSaveReqVO.Document document : documents) { + if (document == null || StrUtil.hasBlank(document.getTitle(), document.getContent()) + || !titles.add(document.getTitle())) { + throw exception(KNOWLEDGE_LIBRARY_TEMPLATE_INVALID); + } + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleService.java new file mode 100644 index 000000000..b99775583 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleService.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.pms.service.kb.recycle; + +import cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo.PmsKnowledgeRecycleDetailRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.recycle.PmsKnowledgeRecycleRecordDO; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * PMS 知识库回收站 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsKnowledgeRecycleService { + + /** + * 将知识库及其全部内容移入回收站,只记录本次显式删除的知识库 + * + * @param library 知识库 + * @param userId 用户编号 + */ + void recycleLibrary(PmsKnowledgeLibraryDO library, Long userId); + + /** + * 将文件夹子树移入回收站,只记录本次显式删除的根文件夹 + * + * @param folder 根文件夹 + * @param folders 根文件夹及其子文件夹列表 + * @param documents 文件夹子树中的文档列表 + * @param userId 用户编号 + */ + void recycleFolder(PmsKnowledgeFolderDO folder, List folders, + List documents, Long userId); + + /** + * 将文档及全部子文档移入回收站,只记录本次显式删除的根文档 + * + * @param document 根文档 + * @param documents 根文档及其子文档列表 + * @param userId 用户编号 + */ + void recycleDocument(PmsKnowledgeDocumentDO document, List documents, Long userId); + + /** + * 获得当前用户删除的知识库回收站记录 + * + * @param userId 用户编号 + * @return 回收站记录列表 + */ + List getLibraryRecycleList(Long userId); + + /** + * 获得知识库的内容回收站记录,只返回当前用户有内容删除权限的记录 + * + * @param libraryId 知识库编号 + * @param userId 用户编号 + * @return 回收站记录列表 + */ + List getContentRecycleList(Long libraryId, Long userId); + + /** + * 获得知识库回收站对象及其级联删除内容 + * + * @param recordId 回收站记录编号 + * @param userId 用户编号 + * @return 回收站详情 + */ + PmsKnowledgeRecycleDetailRespVO getContentRecycleDetail(Long recordId, Long userId); + + /** + * 获得回收站内容预览数据 + * + * @param recordId 回收站记录编号 + * @param entityId 预览的级联内容编号,可为空 + * @param userId 用户编号 + * @return 内容预览数据 + */ + PmsKnowledgeDocumentDO getContentRecyclePreview(Long recordId, Long entityId, Long userId); + + /** + * 按类型恢复回收站记录对应的知识库或内容,保留仍被单独删除的子树 + * + * @param recordId 回收站记录编号 + * @param userId 用户编号 + */ + void restoreContentRecycle(Long recordId, Long userId); + + /** + * 按类型彻底删除回收站记录对应的知识库或内容及其关联数据 + * + * @param recordId 回收站记录编号 + * @param userId 用户编号 + */ + void deleteContentRecycle(Long recordId, Long userId); + + /** + * 彻底清理指定时间前的回收站记录,不校验用户权限 + * + * @param deleteTime 删除截止时间 + * @return 处理的回收站记录数量 + */ + int deleteExpiredRecycleRecords(LocalDateTime deleteTime); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleServiceImpl.java new file mode 100644 index 000000000..bfc6f9555 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleServiceImpl.java @@ -0,0 +1,882 @@ +package cn.iocoder.yudao.module.pms.service.kb.recycle; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo.PmsKnowledgeRecycleDetailRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.recycle.vo.PmsKnowledgeRecycleRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.recycle.PmsKnowledgeRecycleRecordDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.recycle.PmsKnowledgeRecycleRecordMapper; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeContentLevelEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeFolderService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentCommentService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentLikeService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentShareService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeGroupService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.equalsAny; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_ADMIN_REQUIRED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_RECYCLE_RECORD_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_RECYCLE_TYPE_INVALID; + +/** + * PMS 知识库回收站 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsKnowledgeRecycleServiceImpl implements PmsKnowledgeRecycleService { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeLibraryService libraryService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeFolderService folderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsKnowledgeDocumentService documentService; + @Resource + private PmsKnowledgeRecycleRecordMapper recycleRecordMapper; + @Resource + private PmsKnowledgeLibraryMemberService libraryMemberService; + @Resource + private PmsKnowledgeGroupService knowledgeGroupService; + @Resource + private PmsKnowledgeFavoriteService favoriteService; + @Resource + private PmsKnowledgeDocumentLikeService documentLikeService; + @Resource + private PmsKnowledgeViewRecordService viewRecordService; + @Resource + private PmsKnowledgeDocumentShareService documentShareService; + @Resource + private PmsKnowledgeDocumentCommentService documentCommentService; + @Resource + private PmsKnowledgeContentPermissionService contentPermissionService; + + @Resource + private PermissionApi permissionApi; + @Resource + private AdminUserApi adminUserApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public void recycleLibrary(PmsKnowledgeLibraryDO library, Long userId) { + LocalDateTime deleteTime = LocalDateTime.now(); + // 1.1 查询知识库范围内的文件夹和文档 + List folders = folderService.getFolderListByLibraryId(library.getId()); + List documents = documentService.getDocumentListByLibraryId(library.getId()); + + // 2.1 将知识库主记录移入回收站 + libraryService.updateLibraryToRecycled(library.getId(), userId, deleteTime); + // 2.2 将文件夹及文档移入回收站 + if (CollUtil.isNotEmpty(folders)) { + folderService.updateFolderList(convertList(folders, folder -> new PmsKnowledgeFolderDO() + .setId(folder.getId()).setStatus(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus()) + .setDeleteUserId(userId).setDeleteTime(deleteTime))); + } + if (CollUtil.isNotEmpty(documents)) { + documentService.updateDocumentList(convertList(documents, document -> new PmsKnowledgeDocumentDO() + .setId(document.getId()).setStatus(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus()) + .setDeleteUserId(userId).setDeleteTime(deleteTime))); + } + + // 3. 只记录本次显式删除的知识库,级联内容不重复出现在回收站 + insertRecycleRecord(library.getId(), PmsKnowledgeObjectTypeEnum.LIBRARY.getType(), + library.getId(), library.getName(), userId, deleteTime); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void recycleFolder(PmsKnowledgeFolderDO folder, List folders, + List documents, Long userId) { + LocalDateTime deleteTime = LocalDateTime.now(); + // 1. 将文件夹、子文件夹及其中的文档移入回收站 + if (CollUtil.isNotEmpty(documents)) { + documentService.updateDocumentList(convertList(documents, document -> + new PmsKnowledgeDocumentDO().setId(document.getId()) + .setStatus(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus()) + .setDeleteUserId(userId).setDeleteTime(deleteTime))); + } + if (CollUtil.isNotEmpty(folders)) { + folderService.updateFolderList(convertList(folders, item -> + new PmsKnowledgeFolderDO().setId(item.getId()) + .setStatus(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus()) + .setDeleteUserId(userId).setDeleteTime(deleteTime))); + } + + // 2. 只记录本次显式删除的根文件夹 + insertRecycleRecord(folder.getLibraryId(), PmsKnowledgeObjectTypeEnum.FOLDER.getType(), + folder.getId(), folder.getTitle(), userId, deleteTime); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void recycleDocument(PmsKnowledgeDocumentDO document, List documents, + Long userId) { + LocalDateTime deleteTime = LocalDateTime.now(); + // 1. 将文档及全部子文档移入回收站 + if (CollUtil.isNotEmpty(documents)) { + documentService.updateDocumentList(convertList(documents, item -> + new PmsKnowledgeDocumentDO().setId(item.getId()) + .setStatus(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus()) + .setDeleteUserId(userId).setDeleteTime(deleteTime))); + } + + // 2. 只记录本次显式删除的根文档 + insertRecycleRecord(document.getLibraryId(), getDocumentRecycleType(document), + document.getId(), document.getTitle(), userId, deleteTime); + } + + @Override + public List getLibraryRecycleList(Long userId) { + return recycleRecordMapper.selectListByTypeAndDeleteUserId(PmsKnowledgeObjectTypeEnum.LIBRARY.getType(), userId); + } + + @Override + public List getContentRecycleList(Long libraryId, Long userId) { + // 1.1 校验知识库可读 + libraryMemberService.validateLibraryReadable(libraryId, userId); + // 1.2 查询知识库回收站记录,并排除知识库自身记录 + List records = recycleRecordMapper.selectListByLibraryId(libraryId); + records.removeIf(record -> PmsKnowledgeObjectTypeEnum.LIBRARY.getType().equals(record.getType())); + + // 2.1 批量解析内容权限 + Map recordPermissionIdMap = getRecordPermissionIdMap(records); + Set permissionIds = new LinkedHashSet<>(recordPermissionIdMap.values()); + Map levelMap = contentPermissionService.getCurrentUserContentPermissionLevelMap(permissionIds, libraryId, userId); + // 2.2 只展示当前用户仍可删除的回收站记录 + records.removeIf(record -> + !PmsKnowledgeContentLevelEnum.canDelete(levelMap.get(recordPermissionIdMap.get(record.getId())))); + return records; + } + + @Override + public PmsKnowledgeRecycleDetailRespVO getContentRecycleDetail(Long recordId, Long userId) { + // 1. 复用回收站列表的库级与内容删除权限校验,避免详情接口绕过权限边界 + PmsKnowledgeRecycleRecordDO record = validateContentRecycleRecord(recordId, userId); + + // 2. 查询仍处于回收状态的内容,按根对象关系筛出本次级联删除的子树 + List folders = folderService.getFolderListByLibraryId(record.getLibraryId()); + folders.removeIf(folder -> ObjectUtil.notEqual(folder.getStatus(), + PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus())); + List documents = documentService.getDocumentListByLibraryId(record.getLibraryId()); + documents.removeIf(document -> ObjectUtil.notEqual(document.getStatus(), + PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus())); + Set folderIds = collectFolderDescendantIds(record, folders); + Set documentIds = collectDocumentDescendantIds(record, documents, folderIds); + + // 3. 组装平铺详情,前端按 parentId/folderId 渲染递归树 + // 3.1 组装级联文件夹节点,并排除回收站根对象 + List children = convertList(folders, + folder -> new PmsKnowledgeRecycleDetailRespVO.Item() + .setId(folder.getId()).setType(PmsKnowledgeObjectTypeEnum.FOLDER.getType()) + .setName(folder.getTitle()).setParentId(folder.getParentId()) + .setDeleteTime(folder.getDeleteTime()), + folder -> folderIds.contains(folder.getId()) + && ObjectUtil.notEqual(folder.getId(), record.getEntityId())); + // 3.2 组装级联文档节点,并排除回收站根对象 + children.addAll(convertList(documents, + document -> new PmsKnowledgeRecycleDetailRespVO.Item() + .setId(document.getId()).setType(getDocumentRecycleType(document)) + .setName(document.getTitle()).setParentId(document.getParentId()) + .setFolderId(document.getFolderId()).setDeleteTime(document.getDeleteTime()), + document -> documentIds.contains(document.getId()) + && ObjectUtil.notEqual(document.getId(), record.getEntityId()))); + return new PmsKnowledgeRecycleDetailRespVO() + .setRoot(BeanUtils.toBean(record, PmsKnowledgeRecycleRespVO.class)) + .setChildren(children); + } + + @Override + public PmsKnowledgeDocumentDO getContentRecyclePreview(Long recordId, Long entityId, Long userId) { + // 1. 复用回收站内容权限校验,避免通过预览接口读取无权限内容 + PmsKnowledgeRecycleRecordDO record = validateContentRecycleRecord(recordId, userId); + + // 2. 回收站保留原文档数据,正文或文件地址仅用于只读预览 + Long targetEntityId = entityId != null ? entityId : record.getEntityId(); + Integer targetType = record.getType(); + if (entityId != null && ObjectUtil.notEqual(entityId, record.getEntityId())) { + PmsKnowledgeRecycleDetailRespVO detail = getContentRecycleDetail(recordId, userId); + PmsKnowledgeRecycleDetailRespVO.Item item = CollUtil.findOne(detail.getChildren(), child -> child.getId().equals(entityId)); + if (item == null) { + throw exception(KNOWLEDGE_RECYCLE_RECORD_NOT_EXISTS); + } + targetType = item.getType(); + } + if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(targetType)) { + throw exception(KNOWLEDGE_RECYCLE_TYPE_INVALID); + } + PmsKnowledgeDocumentDO document = documentService.getDocument(targetEntityId); + if (document == null) { + throw exception(KNOWLEDGE_RECYCLE_RECORD_NOT_EXISTS); + } + return document; + } + + /** + * 收集回收站记录对应的文件夹及其后代文件夹 + * + * @param record 回收站记录 + * @param folders 知识库文件夹列表 + * @return 文件夹编号集合 + */ + private Set collectFolderDescendantIds(PmsKnowledgeRecycleRecordDO record, + List folders) { + if (ObjectUtil.notEqual(PmsKnowledgeObjectTypeEnum.FOLDER.getType(), record.getType())) { + return Collections.emptySet(); + } + return getFolderDescendantIds(folders, Collections.singleton(record.getEntityId())); + } + + /** + * 收集回收站记录对应的文档及其后代文档 + * + * @param record 回收站记录 + * @param documents 知识库文档列表 + * @param folderIds 回收站文件夹及其后代编号 + * @return 文档编号集合 + */ + private Set collectDocumentDescendantIds(PmsKnowledgeRecycleRecordDO record, + List documents, + Set folderIds) { + // 1. 文档回收记录:收集根文档及其全部子文档 + if (isDocumentRecycleType(record.getType())) { + return getDocumentDescendantIds(documents, Collections.singleton(record.getEntityId())); + } + + // 2. 非文件夹回收记录:没有可关联的级联文档 + if (ObjectUtil.notEqual(PmsKnowledgeObjectTypeEnum.FOLDER.getType(), record.getType())) { + return Collections.emptySet(); + } + // 3. 文件夹回收记录:按级联文件夹编号筛选文档 + if (CollUtil.isEmpty(documents) || CollUtil.isEmpty(folderIds)) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet<>(); + for (PmsKnowledgeDocumentDO document : documents) { + if (folderIds.contains(document.getFolderId())) { + result.add(document.getId()); + } + } + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void restoreContentRecycle(Long recordId, Long userId) { + // 1.1 校验回收站记录存在 + PmsKnowledgeRecycleRecordDO record = validateRecycleRecord(recordId); + validateRecycleType(record.getType()); + + // 2. 按记录类型校验权限并恢复:知识库要求管理员权限,文件夹和文档要求内容删除权限 + if (PmsKnowledgeObjectTypeEnum.LIBRARY.getType().equals(record.getType())) { + validateRecycledLibraryAdmin(record, userId); + restoreLibrary(record); + } else if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(record.getType())) { + validateRecycleContentDeletable(record, userId); + restoreFolder(record); + } else if (isDocumentRecycleType(record.getType())) { + validateRecycleContentDeletable(record, userId); + restoreDocument(record); + } + + // 3. 删除本次回收站记录 + recycleRecordMapper.deleteById(recordId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteContentRecycle(Long recordId, Long userId) { + // 1. 校验回收站记录存在 + PmsKnowledgeRecycleRecordDO record = validateRecycleRecord(recordId); + validateRecycleType(record.getType()); + + // 2.A 知识库类型:要求管理员权限,并彻底删除知识库及其全部关联数据 + if (PmsKnowledgeObjectTypeEnum.LIBRARY.getType().equals(record.getType())) { + validateRecycledLibraryAdmin(record, userId); + permanentDeleteLibrary(record.getLibraryId()); + return; + } + + // 2.B 内容类型:校验内容删除权限后,按文件夹和文档类型分别彻底删除 + validateRecycleContentDeletable(record, userId); + if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(record.getType())) { + permanentDeleteFolder(record); + } else if (isDocumentRecycleType(record.getType())) { + permanentDeleteDocument(record); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteExpiredRecycleRecords(LocalDateTime deleteTime) { + // 1. 按删除时间从早到晚查询到期记录 + List records = recycleRecordMapper.selectListByDeleteTimeBefore(deleteTime); + if (CollUtil.isEmpty(records)) { + return 0; + } + + // 2. 按记录类型执行物理清理;上级对象清理后,跳过已被级联删除的记录 + for (PmsKnowledgeRecycleRecordDO record : records) { + if (recycleRecordMapper.selectById(record.getId()) == null) { + continue; + } + validateRecycleType(record.getType()); + if (PmsKnowledgeObjectTypeEnum.LIBRARY.getType().equals(record.getType())) { + permanentDeleteLibrary(record.getLibraryId()); + } else if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(record.getType())) { + permanentDeleteFolder(record); + } else if (isDocumentRecycleType(record.getType())) { + permanentDeleteDocument(record); + } + } + return records.size(); + } + + /** + * 恢复知识库,并恢复本次知识库删除所级联回收的内容 + * + * @param record 知识库回收站记录 + */ + private void restoreLibrary(PmsKnowledgeRecycleRecordDO record) { + // 1.1 查询并校验仍处于回收站状态的知识库 + PmsKnowledgeLibraryDO library = libraryService.getLibrary(record.getLibraryId()); + if (library == null || ObjectUtil.notEqual(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus(), + library.getStatus())) { + throw exception(KNOWLEDGE_LIBRARY_NOT_EXISTS); + } + // 1.2 查询知识库回收站记录,供后续排除独立删除的子树 + List records = recycleRecordMapper.selectListByLibraryId(library.getId()); + + // 2.1 恢复知识库主记录 + libraryService.restoreLibrary(library.getId()); + // 2.2 恢复随知识库级联删除的内容,保留此前被单独删除的子树 + restoreLibraryContent(library.getId(), records, null); + } + + /** + * 恢复文件夹及其子树,并在父文件夹仍被删除时挂载到知识库根目录 + * + * @param record 文件夹回收站记录 + */ + private void restoreFolder(PmsKnowledgeRecycleRecordDO record) { + // 1. 校验根文件夹仍处于回收站状态 + PmsKnowledgeFolderDO root = folderService.getFolder(record.getEntityId()); + if (root == null || ObjectUtil.notEqual(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus(), + root.getStatus())) { + throw exception(KNOWLEDGE_RECYCLE_RECORD_NOT_EXISTS); + } + // 1.2 查询知识库回收站记录和原父文件夹 + List records = recycleRecordMapper.selectListByLibraryId(record.getLibraryId()); + PmsKnowledgeFolderDO parent = root.getParentId() != null + && ObjectUtil.notEqual(PmsKnowledgeFolderDO.PARENT_ID_ROOT, root.getParentId()) + ? folderService.getFolder(root.getParentId()) : null; + + // 2.1 恢复当前文件夹树,同时跳过其中独立删除的子树 + restoreLibraryContent(record.getLibraryId(), records, root.getId()); + // 2.2 被删除的父文件夹尚未恢复时,将当前根文件夹恢复到知识库根目录 + if (root.getParentId() != null + && ObjectUtil.notEqual(PmsKnowledgeFolderDO.PARENT_ID_ROOT, root.getParentId())) { + if (parent == null || PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus().equals(parent.getStatus())) { + folderService.updateFolderList(Collections.singleton(new PmsKnowledgeFolderDO() + .setId(root.getId()).setParentId(PmsKnowledgeFolderDO.PARENT_ID_ROOT))); + } + } + } + + /** + * 恢复文档及其子文档,并修正失效的目录或父文档引用 + * + * @param record 文档回收站记录 + */ + private void restoreDocument(PmsKnowledgeRecycleRecordDO record) { + // 1. 校验根文档仍处于回收站状态 + PmsKnowledgeDocumentDO root = documentService.getDocument(record.getEntityId()); + if (root == null || ObjectUtil.notEqual(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus(), + root.getStatus())) { + throw exception(KNOWLEDGE_RECYCLE_RECORD_NOT_EXISTS); + } + // 1.2 查询知识库文档全集和回收站记录 + List documents = documentService.getDocumentListByLibraryId(record.getLibraryId()); + List records = recycleRecordMapper.selectListByLibraryId(record.getLibraryId()); + // 1.3 查询文档原目录和父文档 + PmsKnowledgeFolderDO folder = equalsAny(root.getFolderId(), null, PmsKnowledgeDocumentDO.FOLDER_ID_ROOT) ? null + : folderService.getFolder(root.getFolderId()); + PmsKnowledgeDocumentDO parent = equalsAny(root.getParentId(), null, PmsKnowledgeDocumentDO.PARENT_ID_ROOT) ? null + : documentService.getDocument(root.getParentId()); + + // 2.1 恢复文档树,同时保留独立删除的子文档树 + Set restoreIds = getDocumentDescendantIds(documents, Collections.singleton(root.getId())); + excludeProtectedDocuments(restoreIds, documents, records, record.getId()); + restoreDocuments(restoreIds); + // 2.2 原目录或父文档尚未恢复时,将当前根文档恢复到知识库根目录 + boolean invalidFolder = root.getFolderId() != null + && ObjectUtil.notEqual(PmsKnowledgeDocumentDO.FOLDER_ID_ROOT, root.getFolderId()) + && (folder == null || PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus().equals(folder.getStatus())); + boolean invalidParent = root.getParentId() != null + && ObjectUtil.notEqual(PmsKnowledgeDocumentDO.PARENT_ID_ROOT, root.getParentId()) + && (parent == null || PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus().equals(parent.getStatus())); + if (invalidFolder || invalidParent) { + documentService.updateDocumentList(Collections.singleton(new PmsKnowledgeDocumentDO() + .setId(root.getId()).setFolderId(PmsKnowledgeDocumentDO.FOLDER_ID_ROOT) + .setParentId(PmsKnowledgeDocumentDO.PARENT_ID_ROOT))); + } + } + + /** + * 恢复知识库范围内的文件夹和文档,排除仍由其他回收站记录保护的子树 + * + * @param libraryId 知识库编号 + * @param records 知识库回收站记录 + * @param folderRootId 仅恢复指定文件夹子树时的根编号,为 {@code null} 表示整个知识库 + */ + private void restoreLibraryContent(Long libraryId, List records, Long folderRootId) { + // 1.1 查询知识库范围内的文件夹和文档 + List folders = folderService.getFolderListByLibraryId(libraryId); + List documents = documentService.getDocumentListByLibraryId(libraryId); + // 1.2 解析目标文件夹树,并排除被单独删除的文件夹子树 + Set restoreFolderIds = folderRootId == null ? convertSet(folders, PmsKnowledgeFolderDO::getId) + : getFolderDescendantIds(folders, Collections.singleton(folderRootId)); + excludeProtectedFolders(restoreFolderIds, folders, records, folderRootId); + // 1.3 解析需要随文件夹恢复的文档树,并排除被单独删除的文档子树 + Set documentRoots = new LinkedHashSet<>(); + for (PmsKnowledgeDocumentDO document : documents) { + if ((folderRootId == null && equalsAny(document.getFolderId(), null, + PmsKnowledgeDocumentDO.FOLDER_ID_ROOT) + && equalsAny(document.getParentId(), null, PmsKnowledgeDocumentDO.PARENT_ID_ROOT)) + || restoreFolderIds.contains(document.getFolderId())) { + documentRoots.add(document.getId()); + } + } + Set restoreDocumentIds = getDocumentDescendantIds(documents, documentRoots); + excludeProtectedDocuments(restoreDocumentIds, documents, records, null); + + // 2.1 恢复文件夹树 + if (CollUtil.isNotEmpty(restoreFolderIds)) { + folderService.restoreFolderList(restoreFolderIds); + } + // 2.2 恢复文档树 + restoreDocuments(restoreDocumentIds); + } + + /** + * 批量恢复文档状态 + * + * @param restoreIds 待恢复文档编号 + */ + private void restoreDocuments(Set restoreIds) { + if (CollUtil.isEmpty(restoreIds)) { + return; + } + documentService.restoreDocumentList(restoreIds); + } + + /** + * 从恢复集合中排除仍处于独立回收状态的文件夹子树 + * + * @param restoreIds 待恢复文件夹编号集合 + * @param folders 所属知识库文件夹 + * @param records 回收站记录 + * @param currentFolderId 当前正在恢复的根文件夹编号 + */ + private void excludeProtectedFolders(Set restoreIds, List folders, + List records, Long currentFolderId) { + Set protectedRoots = new LinkedHashSet<>(); + for (PmsKnowledgeRecycleRecordDO record : records) { + if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(record.getType()) + && ObjectUtil.notEqual(record.getEntityId(), currentFolderId)) { + protectedRoots.add(record.getEntityId()); + } + } + restoreIds.removeAll(getFolderDescendantIds(folders, protectedRoots)); + } + + /** + * 从恢复集合中排除仍处于独立回收状态的文档子树 + * + * @param restoreIds 待恢复文档编号集合 + * @param documents 所属知识库文档 + * @param records 回收站记录 + * @param currentRecordId 当前正在恢复的回收站记录编号 + */ + private void excludeProtectedDocuments(Set restoreIds, List documents, + List records, Long currentRecordId) { + Set protectedRoots = new LinkedHashSet<>(); + for (PmsKnowledgeRecycleRecordDO record : records) { + if (isDocumentRecycleType(record.getType()) + && ObjectUtil.notEqual(record.getId(), currentRecordId)) { + protectedRoots.add(record.getEntityId()); + } + } + restoreIds.removeAll(getDocumentDescendantIds(documents, protectedRoots)); + } + + /** + * 彻底删除知识库及其全部内容、成员和关联数据 + * + * @param libraryId 知识库编号 + */ + private void permanentDeleteLibrary(Long libraryId) { + // 1.1 查询知识库范围内的文档 + List documents = documentService.getDocumentListByLibraryId(libraryId); + Set documentIds = convertSet(documents, PmsKnowledgeDocumentDO::getId); + // 1.2 查询知识库范围内的文件夹 + Set folderIds = convertSet(folderService.getFolderListByLibraryId(libraryId), + PmsKnowledgeFolderDO::getId); + + // 2.1 删除文档收藏、点赞、浏览、分享和评论 + favoriteService.deleteFavoritesByLibraryId(libraryId); + documentLikeService.deleteLikesByDocumentIds(documentIds); + viewRecordService.deleteViewRecordsByLibraryId(libraryId); + deleteDocuments(documentIds); + // 2.2 删除文件夹主记录 + folderService.deleteFolderList(folderIds); + // 2.3 删除内容权限、回收站记录和知识库分组关系 + contentPermissionService.deleteContentPermissionsByLibraryId(libraryId); + recycleRecordMapper.deleteByLibraryId(libraryId); + knowledgeGroupService.deleteKnowledgeGroupRelationsByLibraryId(libraryId); + + // 3.1 删除知识库成员关系 + libraryMemberService.deleteLibraryMembersByLibraryId(libraryId); + // 3.2 最后删除知识库主记录 + libraryService.deleteLibraryPermanently(libraryId); + } + + /** + * 彻底删除文件夹子树及其中的文档和关联数据 + * + * @param record 文件夹回收站记录 + */ + private void permanentDeleteFolder(PmsKnowledgeRecycleRecordDO record) { + // 1.1 查询文件夹和文档全集 + List folders = folderService.getFolderListByLibraryId(record.getLibraryId()); + List documents = documentService.getDocumentListByLibraryId(record.getLibraryId()); + // 1.2 解析文件夹子树和其中的文档编号 + Set folderIds = getFolderDescendantIds(folders, Collections.singleton(record.getEntityId())); + Set documentRoots = new LinkedHashSet<>(); + for (PmsKnowledgeDocumentDO document : documents) { + if (folderIds.contains(document.getFolderId())) { + documentRoots.add(document.getId()); + } + } + Set documentIds = getDocumentDescendantIds(documents, documentRoots); + // 1.3 收集文件夹和文档对应的内容权限编号 + Set permissionIds = new LinkedHashSet<>(); + for (PmsKnowledgeFolderDO folder : folders) { + if (folderIds.contains(folder.getId())) { + permissionIds.add(folder.getPermissionId()); + } + } + for (PmsKnowledgeDocumentDO document : documents) { + if (documentIds.contains(document.getId())) { + permissionIds.add(document.getPermissionId()); + } + } + + // 2.1 删除收藏、点赞、浏览、分享和评论 + favoriteService.deleteFavoritesByEntityIds(folderIds, documentIds); + documentLikeService.deleteLikesByDocumentIds(documentIds); + viewRecordService.deleteViewRecordsByEntityIds(folderIds, documentIds); + deleteDocuments(documentIds); + // 2.2 删除文档和文件夹主记录 + folderService.deleteFolderList(folderIds); + // 2.3 删除不再被引用的内容权限和回收站记录 + contentPermissionService.deleteUnusedContentPermissions(permissionIds); + recycleRecordMapper.deleteByTypeAndEntityIds( + Collections.singleton(PmsKnowledgeObjectTypeEnum.FOLDER.getType()), folderIds); + if (CollUtil.isNotEmpty(documentIds)) { + recycleRecordMapper.deleteByTypeAndEntityIds(Arrays.asList( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), + PmsKnowledgeObjectTypeEnum.FILE.getType()), documentIds); + } + } + + /** + * 彻底删除文档子树及其互动、权限和回收站记录 + * + * @param record 文档回收站记录 + */ + private void permanentDeleteDocument(PmsKnowledgeRecycleRecordDO record) { + // 1.1 查询知识库文档全集 + List documents = documentService.getDocumentListByLibraryId(record.getLibraryId()); + // 1.2 解析文档子树编号 + Set documentIds = getDocumentDescendantIds(documents, Collections.singleton(record.getEntityId())); + // 1.3 收集文档对应的内容权限编号 + Set permissionIds = new LinkedHashSet<>(); + for (PmsKnowledgeDocumentDO document : documents) { + if (documentIds.contains(document.getId())) { + permissionIds.add(document.getPermissionId()); + } + } + + // 2.1 删除收藏、点赞、浏览、分享和评论 + favoriteService.deleteFavoritesByEntityIds(Collections.emptySet(), documentIds); + documentLikeService.deleteLikesByDocumentIds(documentIds); + viewRecordService.deleteViewRecordsByEntityIds(Collections.emptySet(), documentIds); + deleteDocuments(documentIds); + // 2.2 删除不再被引用的内容权限和回收站记录 + contentPermissionService.deleteUnusedContentPermissions(permissionIds); + recycleRecordMapper.deleteByTypeAndEntityIds(Arrays.asList( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), + PmsKnowledgeObjectTypeEnum.FILE.getType()), documentIds); + } + + /** + * 删除文档及其分享、评论等互动数据 + * + * @param documentIds 文档编号集合 + */ + private void deleteDocuments(Set documentIds) { + if (CollUtil.isEmpty(documentIds)) { + return; + } + // 1. 先删除依赖文档编号的分享和评论 + documentShareService.deleteSharesByDocumentIds(documentIds); + documentCommentService.deleteCommentsByDocumentIds(documentIds); + // 2. 再删除文档主记录 + documentService.deleteDocumentList(documentIds); + } + + /** + * 获得文件夹根节点及其全部后代编号 + * + * @param folders 所属知识库文件夹 + * @param rootIds 根文件夹编号 + * @return 根节点及后代编号集合 + */ + private Set getFolderDescendantIds(List folders, Collection rootIds) { + Set descendantIds = new LinkedHashSet<>(rootIds); + // 层级深度设上限,避免异常循环父子关系导致无限遍历 + for (int level = 0; level < Short.MAX_VALUE; level++) { + boolean changed = false; + for (PmsKnowledgeFolderDO folder : folders) { + if (descendantIds.contains(folder.getParentId()) && descendantIds.add(folder.getId())) { + changed = true; + } + } + if (!changed) { + break; + } + } + return descendantIds; + } + + /** + * 获得文档根节点及其全部后代编号 + * + * @param documents 所属知识库文档 + * @param rootIds 根文档编号 + * @return 根节点及后代编号集合 + */ + private Set getDocumentDescendantIds(List documents, Collection rootIds) { + Set descendantIds = new LinkedHashSet<>(rootIds); + // 层级深度设上限,避免异常循环父子关系导致无限遍历 + for (int level = 0; level < Short.MAX_VALUE; level++) { + boolean changed = false; + for (PmsKnowledgeDocumentDO document : documents) { + if (descendantIds.contains(document.getParentId()) && descendantIds.add(document.getId())) { + changed = true; + } + } + if (!changed) { + break; + } + } + return descendantIds; + } + + private void insertRecycleRecord(Long libraryId, Integer type, Long entityId, String name, + Long userId, LocalDateTime deleteTime) { + recycleRecordMapper.insert(new PmsKnowledgeRecycleRecordDO().setLibraryId(libraryId).setType(type) + .setEntityId(entityId).setName(name).setDeleteUserId(userId).setDeleteTime(deleteTime)); + } + + private PmsKnowledgeRecycleRecordDO validateRecycleRecord(Long id) { + PmsKnowledgeRecycleRecordDO record = recycleRecordMapper.selectById(id); + if (record == null) { + throw exception(KNOWLEDGE_RECYCLE_RECORD_NOT_EXISTS); + } + return record; + } + + /** + * 校验内容回收站记录存在且当前用户仍具备内容删除权限 + * + * @param recordId 回收站记录编号 + * @param userId 当前用户编号 + * @return 已校验的回收站记录 + */ + private PmsKnowledgeRecycleRecordDO validateContentRecycleRecord(Long recordId, Long userId) { + PmsKnowledgeRecycleRecordDO record = validateRecycleRecord(recordId); + if (PmsKnowledgeObjectTypeEnum.LIBRARY.getType().equals(record.getType())) { + throw exception(KNOWLEDGE_RECYCLE_TYPE_INVALID); + } + validateRecycleContentDeletable(record, userId); + return record; + } + + /** + * 批量解析回收站记录对应实体的内容权限编号 + * + * @param records 回收站记录 + * @return 回收站记录编号到权限编号的映射 + */ + private Map getRecordPermissionIdMap(List records) { + // 1. 按实体类型批量收集编号 + Set folderIds = new LinkedHashSet<>(); + Set documentIds = new LinkedHashSet<>(); + for (PmsKnowledgeRecycleRecordDO record : records) { + if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(record.getType())) { + folderIds.add(record.getEntityId()); + } else if (isDocumentRecycleType(record.getType())) { + documentIds.add(record.getEntityId()); + } + } + Map folderPermissionIdMap = new LinkedHashMap<>(); + if (CollUtil.isNotEmpty(folderIds)) { + folderService.getFolderList(folderIds).forEach(folder -> + folderPermissionIdMap.put(folder.getId(), folder.getPermissionId())); + } + Map documentPermissionIdMap = new LinkedHashMap<>(); + if (CollUtil.isNotEmpty(documentIds)) { + documentService.getDocumentList(documentIds).forEach(document -> + documentPermissionIdMap.put(document.getId(), document.getPermissionId())); + } + + // 2. 将实体权限编号映射回回收站记录 + Map recordPermissionIdMap = new LinkedHashMap<>(); + records.forEach(record -> { + Map permissionIdMap = PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(record.getType()) + ? folderPermissionIdMap : documentPermissionIdMap; + recordPermissionIdMap.put(record.getId(), permissionIdMap.get(record.getEntityId())); + }); + return recordPermissionIdMap; + } + + /** + * 校验当前用户是否拥有回收站内容的删除权限 + * + * @param record 回收站记录 + * @param userId 当前用户编号 + */ + private void validateRecycleContentDeletable(PmsKnowledgeRecycleRecordDO record, Long userId) { + // 1.1 先校验知识库可读 + libraryMemberService.validateLibraryReadable(record.getLibraryId(), userId); + + // 1.2 再解析回收对象对应的内容权限 + Long permissionId; + if (PmsKnowledgeObjectTypeEnum.FOLDER.getType().equals(record.getType())) { + PmsKnowledgeFolderDO folder = folderService.getFolder(record.getEntityId()); + permissionId = folder != null ? folder.getPermissionId() : null; + } else { + PmsKnowledgeDocumentDO document = documentService.getDocument(record.getEntityId()); + permissionId = document != null ? document.getPermissionId() : null; + } + if (permissionId == null) { + throw exception(KNOWLEDGE_RECYCLE_RECORD_NOT_EXISTS); + } + // 2. 校验当前用户具备内容删除权限 + contentPermissionService.validateContentPermissionDeletable(permissionId, record.getLibraryId(), userId); + } + + /** + * 校验当前用户是否为知识库创建人、管理员或系统超级管理员 + * + * @param record 知识库回收站记录 + * @param userId 当前用户编号 + */ + private void validateRecycledLibraryAdmin(PmsKnowledgeRecycleRecordDO record, Long userId) { + if (permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData()) { + return; + } + PmsKnowledgeLibraryMemberDO member = + libraryMemberService.getMemberByLibraryIdAndUserId(record.getLibraryId(), userId); + if (member == null) { + AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); + if (user != null && user.getDeptId() != null) { + member = libraryMemberService.getMemberByLibraryIdAndDeptId(record.getLibraryId(), user.getDeptId()); + } + } + if (member == null || !equalsAny(member.getLevel(), PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel(), + PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel())) { + throw exception(KNOWLEDGE_LIBRARY_ADMIN_REQUIRED); + } + } + + /** + * 将文档类型映射为回收站对象类型 + * + * @param document 文档 + * @return 文件或普通文档对应的回收站类型 + */ + private Integer getDocumentRecycleType(PmsKnowledgeDocumentDO document) { + return PmsKnowledgeDocumentTypeEnum.FILE.getType().equals(document.getType()) + ? PmsKnowledgeObjectTypeEnum.FILE.getType() : PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(); + } + + /** + * 判断回收站记录是否为文档类对象 + * + * @param type 回收站对象类型 + * @return 是否为文档或文件 + */ + private boolean isDocumentRecycleType(Integer type) { + return equalsAny(type, PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), + PmsKnowledgeObjectTypeEnum.FILE.getType()); + } + + /** + * 校验回收站记录类型,避免未知类型进入内容权限和实体查询分支 + * + * @param type 回收站对象类型 + */ + private void validateRecycleType(Integer type) { + if (equalsAny(type, PmsKnowledgeObjectTypeEnum.LIBRARY.getType(), + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), + PmsKnowledgeObjectTypeEnum.FILE.getType())) { + return; + } + throw exception(KNOWLEDGE_RECYCLE_TYPE_INVALID); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationService.java new file mode 100644 index 000000000..806540fa3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationService.java @@ -0,0 +1,156 @@ +package cn.iocoder.yudao.module.pms.service.pm.iteration; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationOverviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationStartReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * PMS 项目迭代 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsIterationService { + + /** + * 创建项目迭代 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @return 迭代编号 + */ + Long createIteration(PmsIterationSaveReqVO saveReqVO, Long userId); + + /** + * 更新项目迭代 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + */ + void updateIteration(PmsIterationSaveReqVO saveReqVO, Long userId); + + /** + * 开始项目迭代 + * + * @param startReqVO 开始信息 + * @param userId 用户编号 + */ + void startIteration(PmsIterationStartReqVO startReqVO, Long userId); + + /** + * 完成项目迭代 + * + * @param id 迭代编号 + * @param userId 用户编号 + */ + void completeIteration(Long id, Long userId); + + /** + * 删除项目迭代 + * + * @param id 迭代编号 + * @param userId 用户编号 + */ + void deleteIteration(Long id, Long userId); + + /** + * 删除指定项目的全部迭代 + * + * @param projectId 项目编号 + */ + void deleteIterationListByProjectId(Long projectId); + + /** + * 获得项目迭代 + * + * @param id 迭代编号 + * @param userId 用户编号 + * @return 项目迭代 + */ + PmsIterationDO getIteration(Long id, Long userId); + + /** + * 获得项目迭代概况 + * + * @param id 迭代编号 + * @param userId 用户编号 + * @return 迭代概况 + */ + PmsIterationOverviewRespVO getIterationOverview(Long id, Long userId); + + /** + * 获得项目迭代分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 项目迭代分页 + */ + PageResult getIterationPage(PmsIterationPageReqVO pageReqVO, Long userId); + + /** + * 获得迭代完成进度 Map + * + * @param ids 迭代编号集合 + * @return 迭代编号到完成进度 Map + */ + Map getIterationProgressMap(Collection ids); + + /** + * 获得指定编号的项目迭代列表 + * + * @param ids 迭代编号集合 + * @return 项目迭代列表 + */ + List getIterationList(Collection ids); + + /** + * 获得项目迭代 Map + * + * @param ids 迭代编号集合 + * @return 项目迭代 Map + */ + default Map getIterationMap(Collection ids) { + return convertMap(getIterationList(ids), PmsIterationDO::getId); + } + + /** + * 获得指定项目范围内的未完成迭代分页 + * + * @param pageReqVO 工作台分页条件 + * @param projectIds 项目编号集合 + * @param ownerUserId 负责人用户编号 + * @return 迭代分页 + */ + PageResult getUncompletedIterationPage(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long ownerUserId); + + /** + * 获得指定项目范围内的未完成迭代数量 + * + * @param pageReqVO 工作台查询条件 + * @param projectIds 项目编号集合 + * @param ownerUserId 负责人用户编号 + * @return 迭代数量 + */ + Long getUncompletedIterationCount(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long ownerUserId); + + /** + * 获得指定项目范围内按状态分组的迭代数量 + * + * @param projectIds 项目编号集合 + * @return 项目编号、状态和数量的映射 + */ + Map> getProjectIterationStatusCountMap(Collection projectIds); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationServiceImpl.java new file mode 100644 index 000000000..5753a17ee --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationServiceImpl.java @@ -0,0 +1,395 @@ +package cn.iocoder.yudao.module.pms.service.pm.iteration; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationOverviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationStartReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemActivityDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.iteration.PmsIterationMapper; +import cn.iocoder.yudao.module.pms.enums.pm.iteration.PmsIterationStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemActivityService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemWorkLogService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.sum; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.ITERATION_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.ITERATION_STATUS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; + +/** + * PMS 项目迭代 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsIterationServiceImpl implements PmsIterationService { + + /** + * 完成趋势统计天数 + */ + private static final int TREND_DAYS = 14; + /** + * 最近活动最大展示数量 + */ + private static final int RECENT_ACTIVITY_LIMIT = 50; + + @Resource + private PmsIterationMapper iterationMapper; + + @Resource + private PmsProjectMemberService projectMemberService; + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsWorkItemService workItemService; + @Resource + private PmsWorkItemActivityService workItemActivityService; + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsWorkItemWorkLogService workItemWorkLogService; + + @Override + public Long createIteration(PmsIterationSaveReqVO saveReqVO, Long userId) { + // 1.1 校验项目可编辑 + validateProjectWritable(saveReqVO.getProjectId(), userId); + // 1.2 校验负责人 + validateOwnerUser(saveReqVO.getProjectId(), saveReqVO.getOwnerUserId()); + + // 2. 创建未开始的迭代 + Integer maxSort = iterationMapper.selectMaxSortByProjectId(saveReqVO.getProjectId()); + PmsIterationDO iteration = BeanUtils.toBean(saveReqVO, PmsIterationDO.class) + .setStatus(PmsIterationStatusEnum.PLANNED.getStatus()) + .setSort(maxSort == null ? 1 : maxSort + 1); + iterationMapper.insert(iteration); + return iteration.getId(); + } + + @Override + public void updateIteration(PmsIterationSaveReqVO saveReqVO, Long userId) { + // 1.1 校验迭代存在 + PmsIterationDO iteration = validateIterationExists(saveReqVO.getId()); + // 1.2 校验项目可编辑 + validateProjectWritable(iteration.getProjectId(), userId); + // 1.3 校验负责人 + validateOwnerUser(iteration.getProjectId(), saveReqVO.getOwnerUserId()); + + // 2. 更新迭代基本信息 + iterationMapper.updateById(BeanUtils.toBean(saveReqVO, PmsIterationDO.class) + .setProjectId(null)); + } + + @Override + public void startIteration(PmsIterationStartReqVO startReqVO, Long userId) { + // 1.1 校验迭代存在 + PmsIterationDO iteration = validateIterationExists(startReqVO.getId()); + // 1.2 校验项目可编辑 + validateProjectWritable(iteration.getProjectId(), userId); + // 1.3 校验迭代处于未开始状态 + if (ObjectUtil.notEqual(PmsIterationStatusEnum.PLANNED.getStatus(), iteration.getStatus())) { + throw exception(ITERATION_STATUS_INVALID); + } + + // 2. 开始迭代并保存周期 + iterationMapper.updateById(BeanUtils.toBean(startReqVO, PmsIterationDO.class) + .setId(iteration.getId()).setProjectId(null).setStatus(PmsIterationStatusEnum.ACTIVE.getStatus())); + } + + @Override + public void completeIteration(Long id, Long userId) { + // 1.1 校验迭代存在 + PmsIterationDO iteration = validateIterationExists(id); + // 1.2 校验项目可编辑 + validateProjectWritable(iteration.getProjectId(), userId); + // 1.3 校验迭代处于进行中状态 + if (ObjectUtil.notEqual(PmsIterationStatusEnum.ACTIVE.getStatus(), iteration.getStatus())) { + throw exception(ITERATION_STATUS_INVALID); + } + + // 2. 完成迭代 + iterationMapper.updateById(new PmsIterationDO().setId(id) + .setStatus(PmsIterationStatusEnum.COMPLETED.getStatus()).setFinishTime(LocalDateTime.now())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteIteration(Long id, Long userId) { + // 1.1 校验迭代存在 + PmsIterationDO iteration = validateIterationExists(id); + // 1.2 校验项目可编辑 + validateProjectWritable(iteration.getProjectId(), userId); + + // 2. 解除工作项的所属迭代关系后删除迭代 + workItemService.clearWorkItemIterationId(id); + iterationMapper.deleteById(id); + } + + @Override + public void deleteIterationListByProjectId(Long projectId) { + iterationMapper.deleteByProjectId(projectId); + } + + @Override + public PmsIterationDO getIteration(Long id, Long userId) { + // 1. 校验迭代存在 + PmsIterationDO iteration = validateIterationExists(id); + + // 2. 校验项目读取权限 + projectMemberService.validateProjectReadable(iteration.getProjectId(), userId); + return iteration; + } + + @Override + public PmsIterationOverviewRespVO getIterationOverview(Long id, Long userId) { + // 1.1 查询可访问的迭代 + PmsIterationDO iteration = getIteration(id, userId); + // 1.2 查询迭代关联的工作项 + List workItems = workItemService.getActiveWorkItemListByIterationId(id); + + // 2. 统计状态、类型和完成趋势 + int pendingCount = CollUtil.count(workItems, item -> + PmsWorkItemStatusTypeEnum.PENDING.getType().equals(item.getStatus())); + int processingCount = CollUtil.count(workItems, item -> + PmsWorkItemStatusTypeEnum.PROCESSING.getType().equals(item.getStatus())); + int completedCount = CollUtil.count(workItems, item -> + PmsWorkItemStatusTypeEnum.COMPLETED.getType().equals(item.getStatus())); + Map typeCountMap = new LinkedHashMap<>(); + Map> typeStatusCountMap = new LinkedHashMap<>(); + workItems.forEach(item -> { + typeCountMap.merge(item.getType(), 1, Integer::sum); + typeStatusCountMap.computeIfAbsent(item.getType(), key -> new LinkedHashMap<>()) + .merge(item.getStatus(), 1, Integer::sum); + }); + + // 3. 计算状态趋势、燃尽数据和最近活动 + int progress = CollUtil.isEmpty(workItems) ? 0 : completedCount * 100 / workItems.size(); + return new PmsIterationOverviewRespVO().setTotalCount(workItems.size()).setPendingCount(pendingCount) + .setProcessingCount(processingCount).setCompletedCount(completedCount).setProgress(progress) + .setTypeCountMap(typeCountMap).setTypeStatusCountMap(typeStatusCountMap) + .setStatusTrends(buildStatusTrends(workItems)) + .setBurnDowns(buildBurnDowns(iteration, workItems)).setRecentActivities(buildRecentActivities(workItems)); + } + + /** + * 构建迭代内工作项的最近活动列表 + * + * @param workItems 工作项列表 + * @return 最近活动列表 + */ + private List buildRecentActivities(List workItems) { + Map workItemMap = convertMap(workItems, PmsWorkItemDO::getId); + List activities = workItemActivityService.getWorkItemActivityListByWorkItemIds( + convertSet(workItems, PmsWorkItemDO::getId), RECENT_ACTIVITY_LIMIT); + return convertList(activities, activity -> { + PmsWorkItemDO workItem = workItemMap.get(activity.getWorkItemId()); + return new PmsIterationOverviewRespVO.ActivityItem().setId(activity.getId()) + .setWorkItemId(activity.getWorkItemId()).setWorkItemSerialNumber(workItem.getSerialNumber()) + .setWorkItemName(workItem.getName()).setOperatorUserId(activity.getOperatorUserId()) + .setContent(activity.getContent()).setCreateTime(activity.getCreateTime()); + }); + } + + /** + * 构建最近固定天数的每日状态趋势 + * + * @param workItems 工作项列表 + * @return 状态趋势列表 + */ + private List buildStatusTrends(List workItems) { + LocalDate beginDate = LocalDate.now().minusDays(TREND_DAYS - 1L); + List statusTrends = new ArrayList<>(); + for (int index = 0; index < TREND_DAYS; index++) { + LocalDate date = beginDate.plusDays(index); + int pendingCount = CollUtil.count(workItems, item -> + PmsWorkItemStatusTypeEnum.PENDING.getType().equals(item.getStatus()) + && item.getUpdateTime() != null && date.equals(item.getUpdateTime().toLocalDate())); + int processingCount = CollUtil.count(workItems, item -> + PmsWorkItemStatusTypeEnum.PROCESSING.getType().equals(item.getStatus()) + && item.getUpdateTime() != null && date.equals(item.getUpdateTime().toLocalDate())); + int completedCount = CollUtil.count(workItems, item -> + PmsWorkItemStatusTypeEnum.COMPLETED.getType().equals(item.getStatus()) + && item.getUpdateTime() != null && date.equals(item.getUpdateTime().toLocalDate())); + statusTrends.add(new PmsIterationOverviewRespVO.TrendItem().setDate(date.toString()) + .setPendingCount(pendingCount).setProcessingCount(processingCount) + .setCompletedCount(completedCount)); + } + return statusTrends; + } + + /** + * 根据迭代周期和工时登记记录构建燃尽数据 + * + * @param iteration 迭代 + * @param workItems 工作项列表 + * @return 燃尽数据列表 + */ + private List buildBurnDowns(PmsIterationDO iteration, + List workItems) { + // 1. 未配置完整迭代周期时,不生成燃尽数据 + if (iteration.getStartTime() == null || iteration.getEndTime() == null) { + return new ArrayList<>(); + } + // 2. 汇总迭代工作项的预估工时和每日实际投入工时 + int totalEstimatedHours = (int) sum(workItems, + item -> item.getEstimatedHours() == null ? 0 : item.getEstimatedHours()); + Map actualHoursByDate = new LinkedHashMap<>(); + List workItemIds = convertList(workItems, PmsWorkItemDO::getId); + workItemWorkLogService.getWorkItemWorkLogListByWorkItemIds(workItemIds).forEach(workLog -> { + if (workLog.getCreateTime() == null || workLog.getActualHours() == null) { + return; + } + LocalDate logDate = workLog.getCreateTime().toLocalDate(); + actualHoursByDate.merge(logDate, workLog.getActualHours(), Integer::sum); + }); + + // 3. 生成限定长度的日期轴,并逐日计算理想和实际剩余工时 + LocalDate beginDate = iteration.getStartTime().toLocalDate(); + LocalDate endDate = iteration.getEndTime().toLocalDate(); + long totalDays = Math.max(1, ChronoUnit.DAYS.between(beginDate, endDate)); + List burnDowns = new ArrayList<>(); + int actualHours = 0; + for (LocalDate date = beginDate; !date.isAfter(endDate); date = date.plusDays(1)) { + long elapsedDays = ChronoUnit.DAYS.between(beginDate, date); + int idealRemaining = (int) Math.ceil(totalEstimatedHours * (totalDays - elapsedDays) * 1.0D / totalDays); + actualHours += actualHoursByDate.getOrDefault(date, 0); + burnDowns.add(new PmsIterationOverviewRespVO.BurnDownItem().setDate(date.toString()) + .setIdealRemaining(idealRemaining).setActualRemaining(totalEstimatedHours - actualHours)); + } + return burnDowns; + } + + @Override + public PageResult getIterationPage(PmsIterationPageReqVO pageReqVO, Long userId) { + // 1. 校验项目读取权限 + projectMemberService.validateProjectReadable(pageReqVO.getProjectId(), userId); + + // 2. 查询迭代分页 + return iterationMapper.selectPage(pageReqVO); + } + + @Override + public Map getIterationProgressMap(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyMap(); + } + // 1. 批量聚合迭代关联的正常工作项状态数量 + Map> iterationStatusCountMap = workItemService.getIterationWorkItemStatusCountMap(ids); + + // 2. 计算每个迭代的完成进度 + Map progressMap = new LinkedHashMap<>(); + ids.forEach(id -> { + Map statusCountMap = iterationStatusCountMap.getOrDefault(id, Collections.emptyMap()); + long totalCount = statusCountMap.values().stream().mapToLong(Long::longValue).sum(); + long completedCount = statusCountMap.getOrDefault(PmsWorkItemStatusTypeEnum.COMPLETED.getType(), 0L); + progressMap.put(id, totalCount == 0 ? 0 : (int) (completedCount * 100 / totalCount)); + }); + return progressMap; + } + + @Override + public List getIterationList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return iterationMapper.selectByIds(ids); + } + + @Override + public PageResult getUncompletedIterationPage(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long ownerUserId) { + if (CollUtil.isEmpty(projectIds)) { + return PageResult.empty(); + } + return iterationMapper.selectWorkbenchPage(pageReqVO, projectIds, pageReqVO.getProjectId(), + pageReqVO.getName(), pageReqVO.getStatus(), pageReqVO.getEndTime(), + PmsIterationStatusEnum.COMPLETED.getStatus(), ownerUserId); + } + + @Override + public Long getUncompletedIterationCount(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long ownerUserId) { + if (CollUtil.isEmpty(projectIds)) { + return 0L; + } + return iterationMapper.selectWorkbenchCount(projectIds, pageReqVO.getProjectId(), pageReqVO.getName(), + pageReqVO.getStatus(), pageReqVO.getEndTime(), + PmsIterationStatusEnum.COMPLETED.getStatus(), ownerUserId); + } + + @Override + public Map> getProjectIterationStatusCountMap(Collection projectIds) { + if (CollUtil.isEmpty(projectIds)) { + return Collections.emptyMap(); + } + return iterationMapper.selectStatusCountMapByProjectIds(projectIds); + } + + /** + * 校验迭代存在 + * + * @param id 迭代编号 + * @return 迭代 + */ + private PmsIterationDO validateIterationExists(Long id) { + PmsIterationDO iteration = iterationMapper.selectById(id); + if (iteration == null) { + throw exception(ITERATION_NOT_EXISTS); + } + return iteration; + } + + /** + * 校验项目可编辑且处于进行中 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + */ + private void validateProjectWritable(Long projectId, Long userId) { + PmsProjectDO project = projectMemberService.validateProjectWritable(projectId, userId); + if (ObjectUtil.notEqual(PmsProjectStatusEnum.ACTIVE.getStatus(), project.getStatus())) { + throw exception(PROJECT_STATUS_INVALID); + } + } + + /** + * 校验迭代负责人是项目成员 + * + * @param projectId 项目编号 + * @param ownerUserId 负责人用户编号 + */ + private void validateOwnerUser(Long projectId, Long ownerUserId) { + if (ownerUserId != null) { + projectMemberService.validateProjectMemberExists(projectId, ownerUserId); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementService.java new file mode 100644 index 000000000..376d12133 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementService.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.announcement.PmsProjectAnnouncementSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectAnnouncementDO; + +import java.util.List; + +/** + * PMS 项目公告 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsProjectAnnouncementService { + + /** + * 创建项目公告 + * + * @param saveReqVO 保存信息 + * @param userId 后台用户编号 + * @return 公告编号 + */ + Long createProjectAnnouncement(PmsProjectAnnouncementSaveReqVO saveReqVO, Long userId); + + /** + * 更新项目公告 + * + * @param saveReqVO 保存信息 + * @param userId 后台用户编号 + */ + void updateProjectAnnouncement(PmsProjectAnnouncementSaveReqVO saveReqVO, Long userId); + + /** + * 删除项目公告 + * + * @param id 公告编号 + * @param userId 后台用户编号 + */ + void deleteProjectAnnouncement(Long id, Long userId); + + /** + * 获得项目公告详情 + * + * @param id 公告编号 + * @param userId 后台用户编号 + * @return 项目公告 + */ + PmsProjectAnnouncementDO getProjectAnnouncement(Long id, Long userId); + + /** + * 获得项目公告列表 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + * @return 公告列表 + */ + List getProjectAnnouncementList(Long projectId, Long userId); + + /** + * 删除项目的全部公告 + * + * @param projectId 项目编号 + */ + void deleteProjectAnnouncementListByProjectId(Long projectId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementServiceImpl.java new file mode 100644 index 000000000..3b4975958 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementServiceImpl.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.announcement.PmsProjectAnnouncementSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectAnnouncementDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectAnnouncementMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ANNOUNCEMENT_NOT_EXISTS; + +/** + * PMS 项目公告 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsProjectAnnouncementServiceImpl implements PmsProjectAnnouncementService { + + @Resource + private PmsProjectAnnouncementMapper announcementMapper; + + @Resource + private PmsProjectMemberService projectMemberService; + + @Override + public Long createProjectAnnouncement(PmsProjectAnnouncementSaveReqVO saveReqVO, Long userId) { + // 1. 校验项目编辑权限 + projectMemberService.validateProjectWritable(saveReqVO.getProjectId(), userId); + + // 2. 创建公告 + PmsProjectAnnouncementDO announcement = BeanUtils.toBean(saveReqVO, PmsProjectAnnouncementDO.class); + announcementMapper.insert(announcement); + return announcement.getId(); + } + + @Override + public void updateProjectAnnouncement(PmsProjectAnnouncementSaveReqVO saveReqVO, Long userId) { + // 1.1 校验公告存在 + PmsProjectAnnouncementDO announcement = validateAnnouncementExists(saveReqVO.getId()); + // 1.2 校验公告属于请求项目 + if (ObjectUtil.notEqual(announcement.getProjectId(), saveReqVO.getProjectId())) { + throw exception(PROJECT_ANNOUNCEMENT_NOT_EXISTS); + } + // 1.3 校验项目编辑权限 + projectMemberService.validateProjectWritable(announcement.getProjectId(), userId); + + // 2. 更新公告 + announcementMapper.updateById(BeanUtils.toBean(saveReqVO, PmsProjectAnnouncementDO.class)); + } + + @Override + public void deleteProjectAnnouncement(Long id, Long userId) { + // 1.1 校验公告存在 + PmsProjectAnnouncementDO announcement = validateAnnouncementExists(id); + // 1.2 校验项目编辑权限 + projectMemberService.validateProjectWritable(announcement.getProjectId(), userId); + + // 2. 删除公告 + announcementMapper.deleteById(id); + } + + @Override + public PmsProjectAnnouncementDO getProjectAnnouncement(Long id, Long userId) { + // 1. 校验公告存在 + PmsProjectAnnouncementDO announcement = validateAnnouncementExists(id); + // 2. 校验项目读取权限 + projectMemberService.validateProjectReadable(announcement.getProjectId(), userId); + return announcement; + } + + @Override + public List getProjectAnnouncementList(Long projectId, Long userId) { + // 1. 校验项目读取权限 + projectMemberService.validateProjectReadable(projectId, userId); + + // 2. 查询项目公告 + return announcementMapper.selectListByProjectId(projectId); + } + + @Override + public void deleteProjectAnnouncementListByProjectId(Long projectId) { + announcementMapper.deleteByProjectId(projectId); + } + + /** + * 校验项目公告存在 + * + * @param id 公告编号 + * @return 项目公告 + */ + private PmsProjectAnnouncementDO validateAnnouncementExists(Long id) { + PmsProjectAnnouncementDO announcement = announcementMapper.selectById(id); + if (announcement == null) { + throw exception(PROJECT_ANNOUNCEMENT_NOT_EXISTS); + } + return announcement; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteService.java new file mode 100644 index 000000000..6a16ae83d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteService.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import java.util.List; + +/** + * PMS 项目收藏 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsProjectFavoriteService { + + /** + * 收藏项目 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + */ + void createProjectFavorite(Long projectId, Long userId); + + /** + * 取消收藏项目 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + */ + void deleteProjectFavorite(Long projectId, Long userId); + + /** + * 获得用户收藏的项目编号列表 + * + * @param userId 后台用户编号 + * @return 项目编号列表 + */ + List getFavoriteProjectIdListByUserId(Long userId); + + /** + * 删除项目的全部收藏关系 + * + * @param projectId 项目编号 + */ + void deleteProjectFavoriteListByProjectId(Long projectId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteServiceImpl.java new file mode 100644 index 000000000..d0713ddb6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteServiceImpl.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectFavoriteDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectFavoriteMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collections; +import java.util.List; + +/** + * PMS 项目收藏 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsProjectFavoriteServiceImpl implements PmsProjectFavoriteService { + + @Resource + private PmsProjectFavoriteMapper favoriteMapper; + @Resource + private PmsProjectMemberService projectMemberService; + + @Override + public void createProjectFavorite(Long projectId, Long userId) { + // 1. 校验用户是项目成员 + projectMemberService.validateProjectMember(projectId, userId); + + // 2. 创建收藏关系,重复请求保持幂等 + if (favoriteMapper.selectByProjectIdAndUserId(projectId, userId) != null) { + return; + } + favoriteMapper.insert(new PmsProjectFavoriteDO().setProjectId(projectId).setUserId(userId)); + } + + @Override + public void deleteProjectFavorite(Long projectId, Long userId) { + // 1. 校验用户是项目成员 + projectMemberService.validateProjectMember(projectId, userId); + + // 2. 删除收藏关系,重复请求保持幂等 + PmsProjectFavoriteDO favorite = favoriteMapper.selectByProjectIdAndUserId(projectId, userId); + if (favorite != null) { + favoriteMapper.deleteById(favorite.getId()); + } + } + + @Override + public List getFavoriteProjectIdListByUserId(Long userId) { + return favoriteMapper.selectProjectIdListByUserId(userId); + } + + @Override + public void deleteProjectFavoriteListByProjectId(Long projectId) { + favoriteMapper.deleteByProjectIds(Collections.singleton(projectId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupService.java new file mode 100644 index 000000000..e2949eeef --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupService.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectGroupDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * PMS 项目分组 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsProjectGroupService { + + /** + * 创建项目分组 + * + * @param createReqVO 创建信息 + * @param userId 后台用户编号 + * @return 项目分组编号 + */ + Long createProjectGroup(PmsProjectGroupSaveReqVO createReqVO, Long userId); + + /** + * 更新项目分组 + * + * @param updateReqVO 更新信息 + * @param userId 后台用户编号 + */ + void updateProjectGroup(PmsProjectGroupSaveReqVO updateReqVO, Long userId); + + /** + * 更新项目分组排序 + * + * @param sortReqVO 排序信息 + * @param userId 后台用户编号 + */ + void updateProjectGroupSort(PmsProjectGroupSortReqVO sortReqVO, Long userId); + + /** + * 删除项目分组 + * + * @param id 项目分组编号 + * @param userId 后台用户编号 + */ + void deleteProjectGroup(Long id, Long userId); + + /** + * 获得当前用户的项目分组列表 + * + * @param userId 后台用户编号 + * @return 项目分组列表 + */ + List getProjectGroupList(Long userId); + + /** + * 获得项目分组的项目数量 Map + * + * @param userId 后台用户编号 + * @param groups 项目分组列表 + * @param projectIds 当前用户参与的进行中项目编号集合 + * @return 项目分组编号与项目数量 Map + */ + Map getProjectGroupCountMap(Long userId, Collection groups, Collection projectIds); + + /** + * 移动项目到个人分组 + * + * @param moveReqVO 移动信息 + * @param userId 后台用户编号 + */ + void moveProjectToGroup(PmsProjectGroupMoveReqVO moveReqVO, Long userId); + + /** + * 按个人项目分组过滤项目编号 + * + * @param groupId 项目分组编号 + * @param userId 后台用户编号 + * @param projectIds 待过滤的项目编号集合 + * @return 过滤后的项目编号列表 + */ + List filterProjectIdListByGroupId(Long groupId, Long userId, Collection projectIds); + + /** + * 删除项目的全部个人分组关系 + * + * @param projectId 项目编号 + */ + void deleteProjectGroupRelationListByProjectId(Long projectId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupServiceImpl.java new file mode 100644 index 000000000..a110f9bf3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupServiceImpl.java @@ -0,0 +1,279 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectGroupDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectGroupRelationDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectGroupMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectGroupRelationMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectGroupTypeEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_GROUP_DEFAULT_CANNOT_DELETE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_GROUP_DEFAULT_CANNOT_MODIFY; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_GROUP_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_GROUP_NOT_EXISTS; + +/** + * PMS 项目分组 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsProjectGroupServiceImpl implements PmsProjectGroupService { + + @Resource + private PmsProjectGroupMapper projectGroupMapper; + @Resource + private PmsProjectGroupRelationMapper projectGroupRelationMapper; + @Resource + private PmsProjectMemberService projectMemberService; + + @Override + public Long createProjectGroup(PmsProjectGroupSaveReqVO createReqVO, Long userId) { + // 1. 校验分组名称唯一 + validateProjectGroupNameDuplicate(userId, createReqVO.getName(), null); + + // 2. 创建自定义分组,并追加到现有分组末尾 + PmsProjectGroupDO lastGroup = projectGroupMapper.selectLastByUserId(userId); + PmsProjectGroupDO group = BeanUtils.toBean(createReqVO, PmsProjectGroupDO.class) + .setUserId(userId) + .setSort(lastGroup == null ? 2 : lastGroup.getSort() + 1) + .setType(PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(group); + return group.getId(); + } + + @Override + public void updateProjectGroup(PmsProjectGroupSaveReqVO updateReqVO, Long userId) { + // 1.1 校验分组存在 + PmsProjectGroupDO group = validateProjectGroupExists(updateReqVO.getId(), userId); + // 1.2 校验分组为自定义分组 + if (ObjectUtil.notEqual(PmsProjectGroupTypeEnum.CUSTOM.getType(), group.getType())) { + throw exception(PROJECT_GROUP_DEFAULT_CANNOT_MODIFY); + } + // 1.3 校验分组名称唯一 + validateProjectGroupNameDuplicate(userId, updateReqVO.getName(), updateReqVO.getId()); + + // 2. 更新分组名称 + projectGroupMapper.updateById(BeanUtils.toBean(updateReqVO, PmsProjectGroupDO.class) + .setUserId(null).setSort(null).setType(null)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateProjectGroupSort(PmsProjectGroupSortReqVO sortReqVO, Long userId) { + // 1. 校验排序列表中的分组都属于当前用户 + Set groupIds = convertSet(sortReqVO.getItems(), PmsProjectGroupSortReqVO.Item::getId); + if (projectGroupMapper.selectListByIdsAndUserId(groupIds, userId).size() != groupIds.size()) { + throw exception(PROJECT_GROUP_NOT_EXISTS); + } + + // 2. 更新全部分组的显示顺序 + projectGroupMapper.updateBatch(convertList(sortReqVO.getItems(), + item -> new PmsProjectGroupDO().setId(item.getId()).setSort(item.getSort()))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteProjectGroup(Long id, Long userId) { + // 1.1 校验分组存在 + PmsProjectGroupDO group = validateProjectGroupExists(id, userId); + // 1.2 校验分组为自定义分组 + if (ObjectUtil.notEqual(PmsProjectGroupTypeEnum.CUSTOM.getType(), group.getType())) { + throw exception(PROJECT_GROUP_DEFAULT_CANNOT_DELETE); + } + + // 2. 删除分组及其项目关系 + projectGroupRelationMapper.deleteByUserIdAndGroupId(userId, id); + projectGroupMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getProjectGroupList(Long userId) { + // 1. 查询当前用户的项目分组 + List groups = projectGroupMapper.selectListByUserId(userId); + + // 2. 确保“全部项目”和“未分组”两个系统分组存在 + ensureDefaultProjectGroupList(userId, groups); + return groups; + } + + @Override + public Map getProjectGroupCountMap(Long userId, Collection groups, + Collection projectIds) { + if (CollUtil.isEmpty(groups)) { + return Collections.emptyMap(); + } + // 1. 聚合统计各自定义分组的进行中项目数量 + Map groupCountMap = CollUtil.isEmpty(projectIds) ? new LinkedHashMap<>() + : projectGroupRelationMapper.selectGroupCountMapByUserIdAndProjectIds(userId, projectIds); + int groupedProjectCount = CollUtil.isEmpty(projectIds) ? 0 + : projectGroupRelationMapper.selectProjectCountByUserIdAndProjectIds(userId, projectIds); + // 2. 补充“全部项目”和“未分组”的项目数量 + int projectCount = CollUtil.size(projectIds); + for (PmsProjectGroupDO group : groups) { + if (PmsProjectGroupTypeEnum.ALL.getType().equals(group.getType())) { + groupCountMap.put(group.getId(), projectCount); + } else if (PmsProjectGroupTypeEnum.UNGROUPED.getType().equals(group.getType())) { + groupCountMap.put(group.getId(), projectCount - groupedProjectCount); + } + } + return groupCountMap; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void moveProjectToGroup(PmsProjectGroupMoveReqVO moveReqVO, Long userId) { + // 1. 校验当前用户是项目成员 + projectMemberService.validateProjectMember(moveReqVO.getProjectId(), userId); + + // 2. 移出分组时,删除已有的个人分组关系 + if (moveReqVO.getGroupId() == null) { + projectGroupRelationMapper.deleteByUserIdAndProjectId(userId, moveReqVO.getProjectId()); + return; + } + + // 3. 校验目标分组;移动到“未分组”等同于移出分组,“全部项目”不能作为目标分组 + PmsProjectGroupDO targetGroup = validateProjectGroupExists(moveReqVO.getGroupId(), userId); + if (PmsProjectGroupTypeEnum.UNGROUPED.getType().equals(targetGroup.getType())) { + projectGroupRelationMapper.deleteByUserIdAndProjectId(userId, moveReqVO.getProjectId()); + return; + } + if (PmsProjectGroupTypeEnum.ALL.getType().equals(targetGroup.getType())) { + throw exception(PROJECT_GROUP_DEFAULT_CANNOT_MODIFY); + } + + // 4. 新增或更新个人项目分组关系 + PmsProjectGroupRelationDO relation = projectGroupRelationMapper.selectByUserIdAndProjectId( + userId, moveReqVO.getProjectId()); + if (relation == null) { + projectGroupRelationMapper.insert(BeanUtils.toBean(moveReqVO, PmsProjectGroupRelationDO.class) + .setUserId(userId).setSort(0)); + return; + } + projectGroupRelationMapper.updateById(BeanUtils.toBean(moveReqVO, PmsProjectGroupRelationDO.class) + .setId(relation.getId()).setUserId(null).setProjectId(null).setSort(null)); + } + + @Override + public List filterProjectIdListByGroupId(Long groupId, Long userId, Collection projectIds) { + if (CollUtil.isEmpty(projectIds) || groupId == null) { + return CollUtil.isEmpty(projectIds) ? Collections.emptyList() + : new ArrayList<>(new LinkedHashSet<>(projectIds)); + } + + // 1. 校验分组存在;情况一:“全部项目”不额外过滤 + PmsProjectGroupDO group = validateProjectGroupExists(groupId, userId); + if (PmsProjectGroupTypeEnum.ALL.getType().equals(group.getType())) { + return new ArrayList<>(new LinkedHashSet<>(projectIds)); + } + + // 2. 获得候选项目编号集合 + Set candidateProjectIds = new LinkedHashSet<>(projectIds); + // 情况二:“未分组”排除已经加入任意自定义分组的项目 + if (PmsProjectGroupTypeEnum.UNGROUPED.getType().equals(group.getType())) { + List relations = projectGroupRelationMapper.selectListByUserIdAndProjectIds( + userId, candidateProjectIds); + candidateProjectIds.removeAll(convertSet(relations, PmsProjectGroupRelationDO::getProjectId)); + return new ArrayList<>(candidateProjectIds); + } + // 情况三:自定义分组只保留属于该分组的项目 + List relations = projectGroupRelationMapper.selectListByUserIdAndGroupId(userId, groupId); + candidateProjectIds.retainAll(convertSet(relations, PmsProjectGroupRelationDO::getProjectId)); + return new ArrayList<>(candidateProjectIds); + } + + @Override + public void deleteProjectGroupRelationListByProjectId(Long projectId) { + projectGroupRelationMapper.deleteByProjectId(projectId); + } + + /** + * 确保用户拥有“全部项目”和“未分组”两个系统分组 + * + * @param userId 后台用户编号 + * @param groups 当前已查询到的项目分组 + */ + private void ensureDefaultProjectGroupList(Long userId, List groups) { + // 1. 检查两个系统分组是否已经存在 + boolean hasAllGroup = CollUtil.findOne(groups, + group -> PmsProjectGroupTypeEnum.ALL.getType().equals(group.getType())) != null; + boolean hasUngroupedGroup = CollUtil.findOne(groups, + group -> PmsProjectGroupTypeEnum.UNGROUPED.getType().equals(group.getType())) != null; + if (hasAllGroup && hasUngroupedGroup) { + return; + } + + // 2. 构造并插入缺失的系统分组 + List defaultGroups = new ArrayList<>(); + if (!hasAllGroup) { + defaultGroups.add(new PmsProjectGroupDO().setUserId(userId) + .setName(PmsProjectGroupTypeEnum.ALL.getName()).setSort(0) + .setType(PmsProjectGroupTypeEnum.ALL.getType())); + } + if (!hasUngroupedGroup) { + defaultGroups.add(new PmsProjectGroupDO().setUserId(userId) + .setName(PmsProjectGroupTypeEnum.UNGROUPED.getName()).setSort(1) + .setType(PmsProjectGroupTypeEnum.UNGROUPED.getType())); + } + projectGroupMapper.insertBatch(defaultGroups); + + // 3. 合并本次新增记录并保持稳定排序 + groups.addAll(defaultGroups); + groups.sort(Comparator.comparing(PmsProjectGroupDO::getSort, + Comparator.nullsLast(Integer::compareTo)).thenComparing(PmsProjectGroupDO::getId)); + } + + /** + * 校验项目分组存在,并且属于当前用户 + * + * @param id 项目分组编号 + * @param userId 后台用户编号 + * @return 项目分组 + */ + private PmsProjectGroupDO validateProjectGroupExists(Long id, Long userId) { + PmsProjectGroupDO group = projectGroupMapper.selectByIdAndUserId(id, userId); + if (group == null) { + throw exception(PROJECT_GROUP_NOT_EXISTS); + } + return group; + } + + /** + * 校验当前用户的项目分组名称唯一 + * + * @param userId 后台用户编号 + * @param name 分组名称 + * @param excludeId 排除的项目分组编号 + */ + private void validateProjectGroupNameDuplicate(Long userId, String name, Long excludeId) { + PmsProjectGroupDO group = projectGroupMapper.selectByUserIdAndName(userId, name, excludeId); + if (group != null) { + throw exception(PROJECT_GROUP_NAME_DUPLICATE); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberService.java new file mode 100644 index 000000000..9af94682d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberService.java @@ -0,0 +1,187 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member.PmsProjectMemberRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member.PmsProjectMemberSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * PMS 项目成员 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsProjectMemberService { + + /** + * 获得用户参与的进行中项目编号 + * + * @param userId 后台用户编号 + * @return 项目编号列表 + */ + List getActiveProjectIdListByUserId(Long userId); + + /** + * 校验用户是指定项目的成员 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + * @return 项目 + */ + PmsProjectDO validateProjectMember(Long projectId, Long userId); + + /** + * 校验用户可以查看指定项目 + * + * 公开项目允许所有后台用户查看,私有项目只允许项目成员或超级管理员查看 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + * @return 项目 + */ + PmsProjectDO validateProjectReadable(Long projectId, Long userId); + + /** + * 校验用户可以编辑指定项目的业务数据 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + * @return 项目 + */ + PmsProjectDO validateProjectWritable(Long projectId, Long userId); + + /** + * 初始化项目成员 + * + * @param projectId 项目编号 + * @param creatorId 创建人编号 + * @param memberUserIds 初始成员编号列表 + */ + void createProjectMemberList(Long projectId, Long creatorId, Collection memberUserIds); + + /** + * 保存项目成员及其权限级别 + * + * @param saveReqVO 保存信息 + * @param operatorUserId 操作人编号 + */ + void updateProjectMemberList(PmsProjectMemberSaveReqVO saveReqVO, Long operatorUserId); + + /** + * 移除项目成员 + * + * @param projectId 项目编号 + * @param userId 成员编号 + * @param operatorUserId 操作人编号 + */ + void deleteProjectMember(Long projectId, Long userId, Long operatorUserId); + + /** + * 主动退出项目 + * + * @param projectId 项目编号 + * @param userId 当前用户编号 + */ + void exitProject(Long projectId, Long userId); + + /** + * 删除项目的全部成员关系 + * + * @param projectId 项目编号 + */ + void deleteProjectMemberListByProjectId(Long projectId); + + /** + * 获得项目成员列表 + * + * @param projectId 项目编号 + * @param userId 当前用户编号 + * @return 项目成员列表 + */ + List getProjectMemberList(Long projectId, Long userId); + + /** + * 获得用户参与的项目编号列表 + * + * @param userId 后台用户编号 + * @return 项目编号列表 + */ + List getProjectIdListByUserId(Long userId); + + /** + * 获得用户拥有的项目编号列表 + * + * @param userId 后台用户编号 + * @return 项目编号列表 + */ + List getOwnerProjectIdListByUserId(Long userId); + + /** + * 获得用户管理的项目编号列表 + * + * @param userId 后台用户编号 + * @return 项目编号列表 + */ + List getManagedProjectIdListByUserId(Long userId); + + /** + * 获得用户可编辑的项目编号列表 + * + * @param userId 后台用户编号 + * @return 项目编号列表 + */ + List getWritableProjectIdListByUserId(Long userId); + + /** + * 获得项目拥有者和管理员用户编号 Map + * + * @param projectIds 项目编号集合 + * @return 项目管理员用户编号 Map + */ + Map> getProjectManagerUserIdListMap(Collection projectIds); + + /** + * 获得项目成员数量 Map + * + * @param projectIds 项目编号集合 + * @return 项目成员数量 Map + */ + Map getProjectMemberCountMap(Collection projectIds); + + /** + * 判断用户是否拥有项目拥有者权限 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + * @return 是否拥有项目拥有者权限 + */ + boolean hasProjectOwnerPermission(Long projectId, Long userId); + + /** + * 判断用户是否拥有项目管理权限 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + * @return 是否拥有项目管理权限 + */ + boolean hasProjectManagerPermission(Long projectId, Long userId); + + /** + * 校验用户是项目成员 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + */ + void validateProjectMemberExists(Long projectId, Long userId); + + /** + * 校验用户列表都是项目成员 + * + * @param projectId 项目编号 + * @param userIds 后台用户编号集合 + */ + void validateProjectMemberList(Long projectId, Collection userIds); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberServiceImpl.java new file mode 100644 index 000000000..4f5678ffa --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberServiceImpl.java @@ -0,0 +1,352 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member.PmsProjectMemberRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member.PmsProjectMemberSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectMemberDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectMemberMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectMemberLevelEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertLinkedSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ADMIN_REQUIRED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_MEMBER_CREATOR_CANNOT_REMOVE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_MEMBER_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_MEMBER_OWNER_CANNOT_EXIT; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_MEMBER_OWNER_LEVEL_CANNOT_ASSIGN; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_WRITE_ACCESS_DENIED; + +/** + * PMS 项目成员 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsProjectMemberServiceImpl implements PmsProjectMemberService { + + @Resource + private PmsProjectMemberMapper projectMemberMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private PmsProjectService projectService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private PermissionApi permissionApi; + + @Override + public List getActiveProjectIdListByUserId(Long userId) { + // 1. 查询用户参与的项目编号并去重 + List members = projectMemberMapper.selectListByUserId(userId); + Set projectIds = convertLinkedSet(members, PmsProjectMemberDO::getProjectId); + if (CollUtil.isEmpty(projectIds)) { + return Collections.emptyList(); + } + + // 2. 只保留进行中的项目 + List projects = projectService.getProjectList(projectIds); + return convertList(projects, PmsProjectDO::getId, + project -> PmsProjectStatusEnum.ACTIVE.getStatus().equals(project.getStatus())); + } + + @Override + public PmsProjectDO validateProjectMember(Long projectId, Long userId) { + // 1. 校验项目存在 + PmsProjectDO project = validateProjectExists(projectId); + + // 2. 校验用户是项目成员 + PmsProjectMemberDO member = projectMemberMapper.selectByProjectIdAndUserId(projectId, userId); + if (member == null) { + throw exception(PROJECT_ACCESS_DENIED); + } + return project; + } + + @Override + public PmsProjectDO validateProjectReadable(Long projectId, Long userId) { + // 1. 校验项目存在 + PmsProjectDO project = validateProjectExists(projectId); + + // 2. 公开项目允许查看,私有项目只允许项目成员或超级管理员查看 + if (Boolean.FALSE.equals(project.getOpenStatus()) + && projectMemberMapper.selectByProjectIdAndUserId(projectId, userId) == null + && !isSuperAdmin(userId)) { + throw exception(PROJECT_ACCESS_DENIED); + } + return project; + } + + @Override + public PmsProjectDO validateProjectWritable(Long projectId, Long userId) { + // 1.1 校验项目存在 + PmsProjectDO project = validateProjectExists(projectId); + // 1.2 超级管理员直接通过 + if (isSuperAdmin(userId)) { + return project; + } + + // 2. 校验项目成员权限级别允许编辑 + PmsProjectMemberDO member = projectMemberMapper.selectByProjectIdAndUserId(projectId, userId); + if (member == null || !PmsProjectMemberLevelEnum.isWritable(member.getLevel())) { + throw exception(PROJECT_WRITE_ACCESS_DENIED); + } + return project; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void createProjectMemberList(Long projectId, Long creatorId, Collection memberUserIds) { + // 1. 校验初始成员 + Set userIds = new LinkedHashSet<>(); + CollectionUtils.addIfNotNull(userIds, creatorId); + if (CollUtil.isNotEmpty(memberUserIds)) { + userIds.addAll(memberUserIds); + } + adminUserApi.validateUserList(userIds).checkError(); + + // 2. 创建人作为项目拥有者,其余成员使用默认编辑级别 + List members = convertList(userIds, userId -> new PmsProjectMemberDO() + .setProjectId(projectId).setUserId(userId) + .setLevel(ObjectUtil.equal(creatorId, userId) ? PmsProjectMemberLevelEnum.OWNER.getLevel() : PmsProjectMemberLevelEnum.WRITE.getLevel())); + projectMemberMapper.insertBatch(members); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateProjectMemberList(PmsProjectMemberSaveReqVO saveReqVO, Long operatorUserId) { + // 1.1 校验操作人可以管理项目 + PmsProjectDO project = validateProjectManager(saveReqVO.getProjectId(), operatorUserId); + // 1.2 校验成员编号不重复 + List userIdList = convertList(saveReqVO.getMembers(), PmsProjectMemberSaveReqVO.Member::getUserId); + Set userIds = new LinkedHashSet<>(userIdList); + if (userIds.size() != userIdList.size()) { + throw exception(PROJECT_MEMBER_NOT_EXISTS); + } + // 1.3 校验成员账号 + adminUserApi.validateUserList(userIds).checkError(); + // 1.4 校验成员权限级别 + Long creatorId = NumberUtils.parseLong(project.getCreator()); + for (PmsProjectMemberSaveReqVO.Member member : saveReqVO.getMembers()) { + if (ObjectUtil.equal(creatorId, member.getUserId())) { + throw exception(PROJECT_MEMBER_CREATOR_CANNOT_REMOVE); + } + if (PmsProjectMemberLevelEnum.isOwner(member.getLevel())) { + throw exception(PROJECT_MEMBER_OWNER_LEVEL_CANNOT_ASSIGN); + } + } + + // 2. 删除后按请求保存每名成员的权限级别 + projectMemberMapper.deleteByProjectIdAndUserIds(saveReqVO.getProjectId(), userIds); + projectMemberMapper.insertBatch(convertList(saveReqVO.getMembers(), member -> + BeanUtils.toBean(member, PmsProjectMemberDO.class).setProjectId(saveReqVO.getProjectId()))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteProjectMember(Long projectId, Long userId, Long operatorUserId) { + // 1.1 校验操作人可以管理项目 + PmsProjectDO project = validateProjectManager(projectId, operatorUserId); + // 1.2 校验待移除成员存在 + PmsProjectMemberDO member = projectMemberMapper.selectByProjectIdAndUserId(projectId, userId); + if (member == null) { + throw exception(PROJECT_MEMBER_NOT_EXISTS); + } + if (ObjectUtil.equal(NumberUtils.parseLong(project.getCreator()), userId) + || PmsProjectMemberLevelEnum.isOwner(member.getLevel())) { + throw exception(PROJECT_MEMBER_CREATOR_CANNOT_REMOVE); + } + + // 2. 移除项目成员 + projectMemberMapper.deleteByProjectIdAndUserId(projectId, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void exitProject(Long projectId, Long userId) { + // 1.1 校验项目可访问 + validateProjectReadable(projectId, userId); + // 1.2 校验当前账号是直接成员 + PmsProjectMemberDO member = projectMemberMapper.selectByProjectIdAndUserId(projectId, userId); + if (member == null) { + throw exception(PROJECT_MEMBER_NOT_EXISTS); + } + // 1.3 校验项目所有者不能主动退出 + if (PmsProjectMemberLevelEnum.isOwner(member.getLevel())) { + throw exception(PROJECT_MEMBER_OWNER_CANNOT_EXIT); + } + + // 2. 退出项目 + projectMemberMapper.deleteByProjectIdAndUserId(projectId, userId); + } + + @Override + public void deleteProjectMemberListByProjectId(Long projectId) { + projectMemberMapper.deleteByProjectId(projectId); + } + + @Override + public List getProjectMemberList(Long projectId, Long userId) { + // 1. 校验当前用户可以访问项目 + PmsProjectDO project = validateProjectReadable(projectId, userId); + + // 2. 转换项目成员 + Long creatorId = NumberUtils.parseLong(project.getCreator()); + List members = projectMemberMapper.selectListByProjectId(projectId); + return convertList(members, member -> new PmsProjectMemberRespVO() + .setUserId(member.getUserId()).setLevel(member.getLevel()) + .setCreatorStatus(ObjectUtil.equal(member.getUserId(), creatorId))); + } + + @Override + public List getProjectIdListByUserId(Long userId) { + List members = projectMemberMapper.selectListByUserId(userId); + return convertList(members, PmsProjectMemberDO::getProjectId); + } + + @Override + public List getOwnerProjectIdListByUserId(Long userId) { + List members = projectMemberMapper.selectListByUserIdAndLevels(userId, + Collections.singleton(PmsProjectMemberLevelEnum.OWNER.getLevel())); + return convertList(members, PmsProjectMemberDO::getProjectId); + } + + @Override + public List getManagedProjectIdListByUserId(Long userId) { + List members = projectMemberMapper.selectListByUserIdAndLevels( + userId, PmsProjectMemberLevelEnum.MANAGER_LEVELS); + return convertList(members, PmsProjectMemberDO::getProjectId); + } + + @Override + public List getWritableProjectIdListByUserId(Long userId) { + List members = projectMemberMapper.selectListByUserIdAndLevels( + userId, PmsProjectMemberLevelEnum.WRITABLE_LEVELS); + return convertList(members, PmsProjectMemberDO::getProjectId); + } + + @Override + public Map> getProjectManagerUserIdListMap(Collection projectIds) { + if (CollUtil.isEmpty(projectIds)) { + return Collections.emptyMap(); + } + List members = projectMemberMapper.selectListByProjectIdsAndLevels( + projectIds, PmsProjectMemberLevelEnum.MANAGER_LEVELS); + return convertMultiMap(members, PmsProjectMemberDO::getProjectId, PmsProjectMemberDO::getUserId); + } + + @Override + public Map getProjectMemberCountMap(Collection projectIds) { + if (CollUtil.isEmpty(projectIds)) { + return Collections.emptyMap(); + } + List members = projectMemberMapper.selectListByProjectIds(projectIds); + return CollUtil.countMap(convertList(members, PmsProjectMemberDO::getProjectId)); + } + + @Override + public boolean hasProjectOwnerPermission(Long projectId, Long userId) { + if (isSuperAdmin(userId)) { + return true; + } + PmsProjectMemberDO member = projectMemberMapper.selectByProjectIdAndUserId(projectId, userId); + return member != null && PmsProjectMemberLevelEnum.isOwner(member.getLevel()); + } + + @Override + public boolean hasProjectManagerPermission(Long projectId, Long userId) { + if (isSuperAdmin(userId)) { + return true; + } + PmsProjectMemberDO member = projectMemberMapper.selectByProjectIdAndUserId(projectId, userId); + return member != null && PmsProjectMemberLevelEnum.isManager(member.getLevel()); + } + + @Override + public void validateProjectMemberExists(Long projectId, Long userId) { + PmsProjectMemberDO member = projectMemberMapper.selectByProjectIdAndUserId(projectId, userId); + if (member == null) { + throw exception(PROJECT_MEMBER_NOT_EXISTS); + } + } + + @Override + public void validateProjectMemberList(Long projectId, Collection userIds) { + if (CollUtil.isEmpty(userIds)) { + return; + } + List members = projectMemberMapper.selectListByProjectId(projectId); + Set projectMemberUserIds = convertSet(members, PmsProjectMemberDO::getUserId); + if (!projectMemberUserIds.containsAll(userIds)) { + throw exception(PROJECT_MEMBER_NOT_EXISTS); + } + } + + /** + * 校验用户可以管理项目 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + * @return 项目 + */ + private PmsProjectDO validateProjectManager(Long projectId, Long userId) { + PmsProjectDO project = validateProjectReadable(projectId, userId); + if (!hasProjectManagerPermission(projectId, userId)) { + throw exception(PROJECT_ADMIN_REQUIRED); + } + return project; + } + + /** + * 判断用户是否为系统超级管理员 + * + * @param userId 后台用户编号 + * @return 是否为超级管理员 + */ + private boolean isSuperAdmin(Long userId) { + return permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData(); + } + + /** + * 校验项目存在 + * + * @param projectId 项目编号 + * @return 项目 + */ + private PmsProjectDO validateProjectExists(Long projectId) { + PmsProjectDO project = projectService.getProject(projectId); + if (project == null) { + throw exception(PROJECT_NOT_EXISTS); + } + return project; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectService.java new file mode 100644 index 000000000..560dff724 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectService.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectOverviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * PMS 项目 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsProjectService { + + /** + * 创建项目 + * + * @param saveReqVO 保存信息 + * @param userId 创建人编号 + * @return 项目编号 + */ + Long createProject(PmsProjectSaveReqVO saveReqVO, Long userId); + + /** + * 更新项目 + * + * @param saveReqVO 保存信息 + * @param userId 操作人编号 + */ + void updateProject(PmsProjectSaveReqVO saveReqVO, Long userId); + + /** + * 获得项目 + * + * @param id 项目编号 + * @return 项目 + */ + PmsProjectDO getProject(Long id); + + /** + * 获得项目详情并记录访问时间 + * + * @param id 项目编号 + * @param userId 当前用户编号 + * @return 项目 + */ + PmsProjectDO getProjectAndUpdateVisitTime(Long id, Long userId); + + /** + * 获得项目分页 + * + * @param pageReqVO 分页条件 + * @param userId 当前用户编号 + * @return 项目分页 + */ + PageResult getProjectPage(PmsProjectPageReqVO pageReqVO, Long userId); + + /** + * 获得当前用户的星标项目列表 + * + * @param userId 当前用户编号 + * @return 星标项目列表 + */ + List getFavoriteProjectList(Long userId); + + /** + * 获得项目列表 + * + * @param ids 项目编号集合 + * @return 项目列表 + */ + List getProjectList(Collection ids); + + /** + * 获得项目 Map + * + * @param ids 项目编号集合 + * @return 项目 Map + */ + default Map getProjectMap(Collection ids) { + return convertMap(getProjectList(ids), PmsProjectDO::getId); + } + + /** + * 获得项目概况 + * + * @param id 项目编号 + * @param userId 当前用户编号 + * @return 项目概况 + */ + PmsProjectOverviewRespVO getProjectOverview(Long id, Long userId); + + /** + * 归档项目 + * + * @param id 项目编号 + * @param userId 操作人编号 + */ + void archiveProject(Long id, Long userId); + + /** + * 将项目移入回收站 + * + * @param id 项目编号 + * @param userId 操作人编号 + */ + void recycleProject(Long id, Long userId); + + /** + * 恢复项目 + * + * @param id 项目编号 + * @param userId 操作人编号 + */ + void restoreProject(Long id, Long userId); + + /** + * 彻底删除回收站项目 + * + * @param id 项目编号 + * @param userId 操作人编号 + */ + void deleteProject(Long id, Long userId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImpl.java new file mode 100644 index 000000000..b5775944b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImpl.java @@ -0,0 +1,379 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectOverviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectSceneTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemStatusService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemWorkLogService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ADMIN_REQUIRED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_OWNER_REQUIRED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; + +/** + * PMS 项目 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsProjectServiceImpl implements PmsProjectService { + + /** + * 项目完成趋势统计天数 + */ + private static final int TREND_DAYS = 14; + /** + * 项目概况展示的待办工作项数量上限 + */ + private static final int ASSIGNED_WORK_ITEM_LIMIT = 10; + + @Resource + private PmsProjectMapper projectMapper; + + @Resource + private PmsProjectMemberService projectMemberService; + @Resource + private PmsProjectGroupService projectGroupService; + @Resource + private PmsProjectFavoriteService projectFavoriteService; + @Resource + private PmsProjectAnnouncementService projectAnnouncementService; + @Resource + private PmsIterationService iterationService; + @Resource + private PmsWorkItemService workItemService; + @Resource + private PmsWorkItemStatusService workItemStatusService; + @Resource + private PmsWorkItemWorkLogService workItemWorkLogService; + + @Resource + private PermissionApi permissionApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createProject(PmsProjectSaveReqVO saveReqVO, Long userId) { + // 1. 创建进行中的项目 + PmsProjectDO project = BeanUtils.toBean(saveReqVO, PmsProjectDO.class) + .setStatus(PmsProjectStatusEnum.ACTIVE.getStatus()).setSort(0).setAccessTime(LocalDateTime.now()); + projectMapper.insert(project); + + // 2.1 创建人作为项目管理员,并初始化其他项目成员 + projectMemberService.createProjectMemberList(project.getId(), userId, saveReqVO.getMemberUserIds()); + // 2.2 初始化项目支持的工作项看板状态 + workItemStatusService.initProjectWorkItemStatuses(project.getId(), project.getType()); + return project.getId(); + } + + @Override + public void updateProject(PmsProjectSaveReqVO saveReqVO, Long userId) { + // 1.1 校验项目存在 + PmsProjectDO project = validateProjectExists(saveReqVO.getId()); + // 1.2 校验项目处于进行中 + validateActiveProject(project); + // 1.3 校验当前用户可以管理项目 + validateProjectManagerPermission(saveReqVO.getId(), userId); + + // 2. 更新项目基本信息 + PmsProjectDO updateObj = BeanUtils.toBean(saveReqVO, PmsProjectDO.class) + .setType(null); + projectMapper.updateById(updateObj); + } + + @Override + public PmsProjectDO getProject(Long id) { + return projectMapper.selectById(id); + } + + @Override + public PmsProjectDO getProjectAndUpdateVisitTime(Long id, Long userId) { + // 1. 校验当前用户可以查看项目 + PmsProjectDO project = projectMemberService.validateProjectReadable(id, userId); + + // 2. 记录最近访问时间 + LocalDateTime accessTime = LocalDateTime.now(); + projectMapper.updateById(new PmsProjectDO().setId(id).setAccessTime(accessTime)); + project.setAccessTime(accessTime); + return project; + } + + @Override + public PageResult getProjectPage(PmsProjectPageReqVO pageReqVO, Long userId) { + List projectIds; + boolean includeOpenProject = false; + boolean includeAllProject = false; + + // 1. 按项目状态和列表场景确定可查询的项目范围 + if (ObjectUtil.notEqual(PmsProjectStatusEnum.ACTIVE.getStatus(), pageReqVO.getStatus())) { + projectIds = projectMemberService.getManagedProjectIdListByUserId(userId); + } else if (PmsProjectSceneTypeEnum.ALL.getType().equals(pageReqVO.getSceneType())) { + projectIds = projectMemberService.getProjectIdListByUserId(userId); + includeOpenProject = true; + includeAllProject = permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode()).getCheckedData(); + } else if (PmsProjectSceneTypeEnum.MANAGED.getType().equals(pageReqVO.getSceneType())) { + projectIds = projectMemberService.getManagedProjectIdListByUserId(userId); + } else { + projectIds = projectGroupService.filterProjectIdListByGroupId(pageReqVO.getGroupId(), userId, + projectMemberService.getProjectIdListByUserId(userId)); + } + if (!includeOpenProject && CollUtil.isEmpty(projectIds)) { + return PageResult.empty(); + } + + // 2. 查询项目分页 + return projectMapper.selectPage(pageReqVO, projectIds, includeOpenProject, includeAllProject); + } + + @Override + public List getFavoriteProjectList(Long userId) { + // 1. 查询当前用户参与且已星标的项目编号 + List favoriteProjectIds = projectFavoriteService.getFavoriteProjectIdListByUserId(userId); + List memberProjectIds = projectMemberService.getProjectIdListByUserId(userId); + List readableFavoriteProjectIds = filterList(favoriteProjectIds, memberProjectIds::contains); + if (CollUtil.isEmpty(readableFavoriteProjectIds)) { + return Collections.emptyList(); + } + // 2. 查询进行中的星标项目列表 + return projectMapper.selectListByIdsAndStatus(readableFavoriteProjectIds, PmsProjectStatusEnum.ACTIVE.getStatus()); + } + + @Override + public List getProjectList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return projectMapper.selectByIds(ids); + } + + @Override + public PmsProjectOverviewRespVO getProjectOverview(Long id, Long userId) { + // 1.1 校验项目可访问 + projectMemberService.validateProjectReadable(id, userId); + // 1.2 查询项目的全部工作项 + List workItems = workItemService.getActiveWorkItemListByProjectId(id); + + // 2. 统计工作项状态和类型 + long pendingCount = 0L; + long processingCount = 0L; + long completedCount = 0L; + Map typeCountMap = new LinkedHashMap<>(); + for (PmsWorkItemDO workItem : workItems) { + typeCountMap.put(workItem.getType(), typeCountMap.getOrDefault(workItem.getType(), 0L) + 1); + if (PmsWorkItemStatusTypeEnum.PENDING.getType().equals(workItem.getStatus())) { + pendingCount++; + } else if (PmsWorkItemStatusTypeEnum.PROCESSING.getType().equals(workItem.getStatus())) { + processingCount++; + } else if (PmsWorkItemStatusTypeEnum.COMPLETED.getType().equals(workItem.getStatus())) { + completedCount++; + } + } + + // 3. 统计近十四日完成趋势 + LocalDate beginDate = LocalDate.now().minusDays(TREND_DAYS - 1L); + Map completedCountMap = new LinkedHashMap<>(); + for (int index = 0; index < TREND_DAYS; index++) { + completedCountMap.put(beginDate.plusDays(index), 0L); + } + for (PmsWorkItemDO workItem : workItems) { + if (PmsWorkItemStatusTypeEnum.COMPLETED.getType().equals(workItem.getStatus()) + && workItem.getUpdateTime() != null + && !workItem.getUpdateTime().toLocalDate().isBefore(beginDate)) { + LocalDate completedDate = workItem.getUpdateTime().toLocalDate(); + completedCountMap.computeIfPresent(completedDate, (date, count) -> count + 1); + } + } + List completedTrends = new ArrayList<>(); + completedCountMap.forEach((date, count) -> completedTrends.add( + new PmsProjectOverviewRespVO.TrendPoint().setDate(date.toString()).setCount(count))); + + // 4. 查询分配给当前用户的未完成工作项 + List assignedWorkItems = new ArrayList<>(); + for (PmsWorkItemDO workItem : workItems) { + if (userId.equals(workItem.getAssigneeUserId()) + && ObjectUtil.notEqual(PmsWorkItemStatusTypeEnum.COMPLETED.getType(), workItem.getStatus())) { + assignedWorkItems.add(workItem); + } + } + assignedWorkItems.sort(Comparator.comparing(PmsWorkItemDO::getEndTime, + Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(PmsWorkItemDO::getId)); + if (assignedWorkItems.size() > ASSIGNED_WORK_ITEM_LIMIT) { + assignedWorkItems = assignedWorkItems.subList(0, ASSIGNED_WORK_ITEM_LIMIT); + } + List assignedWorkItemVOs = new ArrayList<>(); + for (PmsWorkItemDO workItem : assignedWorkItems) { + assignedWorkItemVOs.add(new PmsProjectOverviewRespVO.AssignedWorkItem().setId(workItem.getId()) + .setSerialNumber(workItem.getSerialNumber()).setType(workItem.getType()) + .setName(workItem.getName()).setStatus(workItem.getStatus()).setEndTime(workItem.getEndTime()) + .setProgress(workItem.getProgress())); + } + + // 5. 组装项目概况 + return new PmsProjectOverviewRespVO().setTotalCount((long) workItems.size()) + .setPendingCount(pendingCount).setProcessingCount(processingCount) + .setCompletedCount(completedCount).setTypeCountMap(typeCountMap) + .setCompletedTrends(completedTrends).setAssignedWorkItems(assignedWorkItemVOs); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void archiveProject(Long id, Long userId) { + // 1.1 校验项目存在 + PmsProjectDO project = validateProjectExists(id); + // 1.2 校验项目处于进行中 + validateActiveProject(project); + // 1.3 校验当前用户可以管理项目 + validateProjectManagerPermission(id, userId); + + // 2. 归档项目,并清除个人项目分组和星标关系 + int updateCount = projectMapper.updateStatusAndArchiveTimeById(id, PmsProjectStatusEnum.ACTIVE.getStatus(), + PmsProjectStatusEnum.ARCHIVED.getStatus(), LocalDateTime.now()); + if (updateCount == 0) { + throw exception(PROJECT_STATUS_INVALID); + } + projectGroupService.deleteProjectGroupRelationListByProjectId(id); + projectFavoriteService.deleteProjectFavoriteListByProjectId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void recycleProject(Long id, Long userId) { + // 1.1 校验项目存在 + PmsProjectDO project = validateProjectExists(id); + // 1.2 校验项目处于进行中 + validateActiveProject(project); + // 1.3 校验当前用户可以管理项目 + validateProjectManagerPermission(id, userId); + + // 2. 将项目移入回收站,并清除个人项目分组和星标关系 + int updateCount = projectMapper.updateStatusAndRecycleTimeById(id, PmsProjectStatusEnum.ACTIVE.getStatus(), + PmsProjectStatusEnum.RECYCLED.getStatus(), LocalDateTime.now()); + if (updateCount == 0) { + throw exception(PROJECT_STATUS_INVALID); + } + projectGroupService.deleteProjectGroupRelationListByProjectId(id); + projectFavoriteService.deleteProjectFavoriteListByProjectId(id); + } + + @Override + public void restoreProject(Long id, Long userId) { + // 1.1 校验项目存在 + PmsProjectDO project = validateProjectExists(id); + // 1.2 校验项目已归档或位于回收站 + if (PmsProjectStatusEnum.ACTIVE.getStatus().equals(project.getStatus())) { + throw exception(PROJECT_STATUS_INVALID); + } + // 1.3 校验当前用户可以管理项目 + validateProjectManagerPermission(id, userId); + + // 2. 恢复为进行中的项目,并清空生命周期时间 + int updateCount = projectMapper.updateToRestoreById(id, PmsProjectStatusEnum.ACTIVE.getStatus()); + if (updateCount == 0) { + throw exception(PROJECT_STATUS_INVALID); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteProject(Long id, Long userId) { + // 1.1 校验项目存在 + PmsProjectDO project = validateProjectExists(id); + // 1.2 校验项目位于回收站 + if (ObjectUtil.notEqual(PmsProjectStatusEnum.RECYCLED.getStatus(), project.getStatus())) { + throw exception(PROJECT_STATUS_INVALID); + } + // 1.3 校验当前用户拥有项目所有者权限 + validateProjectOwnerPermission(id, userId); + + // 2. 删除项目工时、工作项、迭代、成员、个人分组关系和项目 + workItemWorkLogService.deleteWorkItemWorkLogListByProjectId(id); + workItemService.deleteWorkItemListByProjectId(id); + iterationService.deleteIterationListByProjectId(id); + projectGroupService.deleteProjectGroupRelationListByProjectId(id); + projectMemberService.deleteProjectMemberListByProjectId(id); + projectFavoriteService.deleteProjectFavoriteListByProjectId(id); + projectAnnouncementService.deleteProjectAnnouncementListByProjectId(id); + projectMapper.deleteById(id); + } + + /** + * 校验当前用户可以管理项目 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + */ + private void validateProjectManagerPermission(Long projectId, Long userId) { + if (!projectMemberService.hasProjectManagerPermission(projectId, userId)) { + throw exception(PROJECT_ADMIN_REQUIRED); + } + } + + /** + * 校验当前用户拥有项目所有者权限 + * + * @param projectId 项目编号 + * @param userId 后台用户编号 + */ + private void validateProjectOwnerPermission(Long projectId, Long userId) { + if (!projectMemberService.hasProjectOwnerPermission(projectId, userId)) { + throw exception(PROJECT_OWNER_REQUIRED); + } + } + + /** + * 校验项目处于进行中 + * + * @param project 项目 + */ + private void validateActiveProject(PmsProjectDO project) { + if (ObjectUtil.notEqual(PmsProjectStatusEnum.ACTIVE.getStatus(), project.getStatus())) { + throw exception(PROJECT_STATUS_INVALID); + } + } + + /** + * 校验项目存在 + * + * @param id 项目编号 + * @return 项目 + */ + private PmsProjectDO validateProjectExists(Long id) { + PmsProjectDO project = projectMapper.selectById(id); + if (project == null) { + throw exception(PROJECT_NOT_EXISTS); + } + return project; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateService.java new file mode 100644 index 000000000..14d2f9576 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateService.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplatePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplateSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectTemplateDO; + +/** + * PMS 项目模板 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsProjectTemplateService { + + /** + * 创建项目模板 + * + * @param createReqVO 创建信息 + * @return 模板编号 + */ + Long createProjectTemplate(PmsProjectTemplateSaveReqVO createReqVO); + + /** + * 更新项目模板 + * + * @param updateReqVO 更新信息 + */ + void updateProjectTemplate(PmsProjectTemplateSaveReqVO updateReqVO); + + /** + * 删除项目模板 + * + * @param id 模板编号 + */ + void deleteProjectTemplate(Long id); + + /** + * 获得项目模板 + * + * @param id 模板编号 + * @return 项目模板 + */ + PmsProjectTemplateDO getProjectTemplate(Long id); + + /** + * 获得项目模板分页 + * + * @param pageReqVO 分页查询 + * @return 项目模板分页 + */ + PageResult getProjectTemplatePage(PmsProjectTemplatePageReqVO pageReqVO); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateServiceImpl.java new file mode 100644 index 000000000..0937ef399 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateServiceImpl.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplatePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplateSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectTemplateDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectTemplateMapper; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_TEMPLATE_CONFIG_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_TEMPLATE_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_TEMPLATE_NOT_EXISTS; + +/** + * PMS 项目模板 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsProjectTemplateServiceImpl implements PmsProjectTemplateService { + + @Resource + private PmsProjectTemplateMapper projectTemplateMapper; + + @Override + public Long createProjectTemplate(PmsProjectTemplateSaveReqVO createReqVO) { + // 1. 校验模板名称和协作配置 + validateProjectTemplateNameDuplicate(createReqVO.getName(), createReqVO.getProjectType(), null); + validateProjectTemplateConfig(createReqVO); + + // 2. 创建项目模板 + PmsProjectTemplateDO template = buildProjectTemplateDO(createReqVO); + projectTemplateMapper.insert(template); + return template.getId(); + } + + @Override + public void updateProjectTemplate(PmsProjectTemplateSaveReqVO updateReqVO) { + // 1. 校验模板存在、名称和协作配置 + validateProjectTemplateExists(updateReqVO.getId()); + validateProjectTemplateNameDuplicate(updateReqVO.getName(), updateReqVO.getProjectType(), updateReqVO.getId()); + validateProjectTemplateConfig(updateReqVO); + + // 2. 更新项目模板 + projectTemplateMapper.updateById(buildProjectTemplateDO(updateReqVO)); + } + + @Override + public void deleteProjectTemplate(Long id) { + // 1. 校验模板存在 + validateProjectTemplateExists(id); + + // 2. 删除项目模板 + projectTemplateMapper.deleteById(id); + } + + @Override + public PmsProjectTemplateDO getProjectTemplate(Long id) { + return validateProjectTemplateExists(id); + } + + @Override + public PageResult getProjectTemplatePage(PmsProjectTemplatePageReqVO pageReqVO) { + return projectTemplateMapper.selectPage(pageReqVO); + } + + /** + * 构建项目模板 DO + * + *

顶层字段使用 BeanUtils 复制,嵌套状态和看板项分别转换为 DO 内部类型后持久化。

+ * + * @param saveReqVO 保存信息 + * @return 项目模板 DO + */ + private PmsProjectTemplateDO buildProjectTemplateDO(PmsProjectTemplateSaveReqVO saveReqVO) { + return BeanUtils.toBean(saveReqVO, PmsProjectTemplateDO.class) + .setStatuses(convertList(saveReqVO.getStatuses(), status -> + BeanUtils.toBean(status, PmsProjectTemplateDO.StatusTemplate.class))) + .setBoards(convertList(saveReqVO.getBoards(), board -> + BeanUtils.toBean(board, PmsProjectTemplateDO.BoardTemplate.class))); + } + + /** + * 校验项目模板存在 + * + * @param id 模板编号 + * @return 项目模板 + */ + private PmsProjectTemplateDO validateProjectTemplateExists(Long id) { + PmsProjectTemplateDO template = projectTemplateMapper.selectById(id); + if (template == null) { + throw exception(PROJECT_TEMPLATE_NOT_EXISTS); + } + return template; + } + + /** + * 校验同一项目类型下的模板名称唯一 + * + * @param name 模板名称 + * @param projectType 项目类型 + * @param id 排除的模板编号 + */ + private void validateProjectTemplateNameDuplicate(String name, Integer projectType, Long id) { + PmsProjectTemplateDO template = projectTemplateMapper + .selectByNameAndProjectType(name, projectType); + if (template == null) { + return; + } + if (id == null) { + throw exception(PROJECT_TEMPLATE_NAME_DUPLICATE); + } + if (ObjectUtil.notEqual(template.getId(), id)) { + throw exception(PROJECT_TEMPLATE_NAME_DUPLICATE); + } + } + + /** + * 校验工作项类型、状态和看板列之间的模板关系 + * + * @param saveReqVO 保存信息 + */ + private void validateProjectTemplateConfig(PmsProjectTemplateSaveReqVO saveReqVO) { + Set itemTypes = new HashSet<>(saveReqVO.getItemTypes()); + if (itemTypes.size() != saveReqVO.getItemTypes().size() + || itemTypes.stream().anyMatch(type -> PmsWorkItemTypeEnum.valueOf(type) == null)) { + throw exception(PROJECT_TEMPLATE_CONFIG_INVALID, "工作项类型存在重复或无效值"); + } + + // 1. 校验状态编码唯一,并且每种工作项只有一个初始状态 + Set statusCodes = new HashSet<>(); + Map defaultStatusCountMap = new HashMap<>(); + for (PmsProjectTemplateSaveReqVO.StatusTemplate status : saveReqVO.getStatuses()) { + if (!itemTypes.contains(status.getWorkItemType()) || !statusCodes.add(status.getCode())) { + throw exception(PROJECT_TEMPLATE_CONFIG_INVALID, "状态类型不属于模板或状态编码重复"); + } + if (Boolean.TRUE.equals(status.getDefaultStatus())) { + defaultStatusCountMap.merge(status.getWorkItemType(), 1, Integer::sum); + } + } + if (itemTypes.stream().anyMatch(type -> ObjectUtil.notEqual(defaultStatusCountMap.get(type), 1))) { + throw exception(PROJECT_TEMPLATE_CONFIG_INVALID, "每种工作项必须且只能配置一个初始状态"); + } + + // 2. 校验看板列编码唯一,且看板列只关联同类型的有效状态 + Set boardCodes = new HashSet<>(); + Map statusMap = new HashMap<>(); + Map statusBoardCodeMap = new HashMap<>(); + saveReqVO.getStatuses().forEach(status -> statusMap.put(status.getCode(), status)); + for (PmsProjectTemplateSaveReqVO.BoardTemplate board : saveReqVO.getBoards()) { + if (!itemTypes.contains(board.getWorkItemType()) || !boardCodes.add(board.getCode()) + || new HashSet<>(board.getStatusCodes()).size() != board.getStatusCodes().size()) { + throw exception(PROJECT_TEMPLATE_CONFIG_INVALID, "看板列类型无效、编码重复或关联状态重复"); + } + for (String statusCode : board.getStatusCodes()) { + PmsProjectTemplateSaveReqVO.StatusTemplate status = statusMap.get(statusCode); + if (status == null || ObjectUtil.notEqual(status.getWorkItemType(), board.getWorkItemType()) + || ObjectUtil.notEqual(status.getBoardCode(), board.getCode()) + || statusBoardCodeMap.putIfAbsent(statusCode, board.getCode()) != null) { + throw exception(PROJECT_TEMPLATE_CONFIG_INVALID, "看板列与状态的类型或编码关系不一致"); + } + } + } + + // 3. 校验每个状态都归属且只归属一个已配置的看板列 + if (saveReqVO.getStatuses().stream().anyMatch(status -> !boardCodes.contains(status.getBoardCode()) + || ObjectUtil.notEqual(statusBoardCodeMap.get(status.getCode()), status.getBoardCode()))) { + throw exception(PROJECT_TEMPLATE_CONFIG_INVALID, "状态没有关联看板列或关联了多个看板列"); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchService.java new file mode 100644 index 000000000..bff4c8fe7 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchService.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.pms.service.pm.workbench; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchCountRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; + +/** + * PMS 工作台 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkbenchService { + + /** + * 获得当前用户在活跃项目中被分配的工作项分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 工作项分页 + */ + PageResult getWorkbenchWorkItemPage(PmsWorkbenchPageReqVO pageReqVO, Long userId); + + /** + * 获得当前用户参与的活跃项目的未完成迭代分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 迭代分页 + */ + PageResult getWorkbenchIterationPage(PmsWorkbenchPageReqVO pageReqVO, Long userId); + + /** + * 获得工作台待办数量,按工作项类型和迭代统计 + * + * @param pageReqVO 查询条件 + * @param userId 用户编号 + * @return 工作台数量 + */ + PmsWorkbenchCountRespVO getWorkbenchCount(PmsWorkbenchPageReqVO pageReqVO, Long userId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchServiceImpl.java new file mode 100644 index 000000000..5da2a863a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchServiceImpl.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.module.pms.service.pm.workbench; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchCountRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Map; + +/** + * PMS 工作台 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsWorkbenchServiceImpl implements PmsWorkbenchService { + + @Resource + private PmsWorkItemService workItemService; + @Resource + private PmsIterationService iterationService; + + @Resource + private PmsProjectMemberService projectMemberService; + + @Override + public PageResult getWorkbenchWorkItemPage(PmsWorkbenchPageReqVO pageReqVO, Long userId) { + // 1. 获得当前用户参与的活跃项目 + List projectIds = projectMemberService.getActiveProjectIdListByUserId(userId); + if (CollUtil.isEmpty(projectIds) || pageReqVO.getProjectId() != null + && !projectIds.contains(pageReqVO.getProjectId())) { + return PageResult.empty(); + } + + // 2. 查询当前用户被分配的未完成工作项分页 + return workItemService.getAssignedWorkItemPage(pageReqVO, projectIds, userId); + } + + @Override + public PageResult getWorkbenchIterationPage(PmsWorkbenchPageReqVO pageReqVO, Long userId) { + // 1. 获得当前用户参与的活跃项目 + List projectIds = projectMemberService.getActiveProjectIdListByUserId(userId); + if (CollUtil.isEmpty(projectIds) || pageReqVO.getProjectId() != null + && !projectIds.contains(pageReqVO.getProjectId())) { + return PageResult.empty(); + } + + // 2. 查询活跃项目的未完成迭代分页 + return iterationService.getUncompletedIterationPage(pageReqVO, projectIds, userId); + } + + @Override + public PmsWorkbenchCountRespVO getWorkbenchCount(PmsWorkbenchPageReqVO pageReqVO, Long userId) { + // 1. 获得当前用户参与的活跃项目 + List projectIds = projectMemberService.getActiveProjectIdListByUserId(userId); + if (CollUtil.isEmpty(projectIds) || pageReqVO.getProjectId() != null + && !projectIds.contains(pageReqVO.getProjectId())) { + return new PmsWorkbenchCountRespVO().setRequirementCount(0).setTaskCount(0) + .setDefectCount(0).setIterationCount(0); + } + + // 2. 按工作项类型和迭代统计当前用户的待办数量 + Map typeCountMap = workItemService.getAssignedWorkItemTypeCountMap(pageReqVO, projectIds, userId); + int iterationCount = iterationService.getUncompletedIterationCount(pageReqVO, projectIds, userId).intValue(); + return new PmsWorkbenchCountRespVO() + .setRequirementCount(typeCountMap.getOrDefault(PmsWorkItemTypeEnum.REQUIREMENT.getType(), 0L).intValue()) + .setTaskCount(typeCountMap.getOrDefault(PmsWorkItemTypeEnum.TASK.getType(), 0L).intValue()) + .setDefectCount(typeCountMap.getOrDefault(PmsWorkItemTypeEnum.DEFECT.getType(), 0L).intValue()) + .setIterationCount(iterationCount); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityService.java new file mode 100644 index 000000000..bea68fb11 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityService.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemActivityDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 工作项动态 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkItemActivityService { + + /** + * 按字段记录工作项更新动态 + * + * @param oldWorkItem 更新前工作项 + * @param newWorkItem 更新后工作项 + * @param oldMemberUserIds 更新前参与人用户编号 + * @param newMemberUserIds 更新后参与人用户编号 + * @param userId 操作人编号 + */ + void createWorkItemUpdateActivities(PmsWorkItemDO oldWorkItem, PmsWorkItemDO newWorkItem, + Collection oldMemberUserIds, Collection newMemberUserIds, + Long userId); + + /** + * 字段发生变化时记录工作项动态 + * + * @param workItem 工作项 + * @param userId 操作人编号 + * @param fieldName 字段名称 + * @param oldFieldValue 旧字段值 + * @param newFieldValue 新字段值 + * @param oldValue 旧展示值 + * @param newValue 新展示值 + */ + void createWorkItemFieldActivityIfChanged(PmsWorkItemDO workItem, Long userId, String fieldName, + Object oldFieldValue, Object newFieldValue, + String oldValue, String newValue); + + /** + * 按迭代编号变化记录工作项动态 + * + * @param workItem 工作项 + * @param userId 操作人编号 + * @param oldIterationId 更新前迭代编号 + * @param newIterationId 更新后迭代编号 + */ + void createWorkItemIterationActivityIfChanged(PmsWorkItemDO workItem, Long userId, + Long oldIterationId, Long newIterationId); + + /** + * 记录一条工作项动态 + * + * @param projectId 项目编号 + * @param workItemId 工作项编号 + * @param operatorUserId 操作用户编号 + * @param content 动态内容枚举 + * @param arguments 动态内容参数 + */ + void createWorkItemActivity(Long projectId, Long workItemId, Long operatorUserId, + PmsWorkItemActivityContentEnum content, Object... arguments); + + /** + * 获得工作项的动态列表 + * + * @param workItemId 工作项编号 + * @return 动态列表 + */ + List getWorkItemActivityList(Long workItemId); + + /** + * 获得多个工作项的最近动态列表 + * + * @param workItemIds 工作项编号集合 + * @param limit 最大数量 + * @return 工作项动态列表 + */ + List getWorkItemActivityListByWorkItemIds(Collection workItemIds, int limit); + + /** + * 删除工作项的全部动态 + * + * @param workItemId 工作项编号 + */ + void deleteWorkItemActivityListByWorkItemId(Long workItemId); + + /** + * 删除项目的全部动态 + * + * @param projectId 项目编号 + */ + void deleteWorkItemActivityListByProjectId(Long projectId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityServiceImpl.java new file mode 100644 index 000000000..98c855745 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityServiceImpl.java @@ -0,0 +1,240 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemActivityDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemActivityMapper; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemDefectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * PMS 工作项动态 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsWorkItemActivityServiceImpl implements PmsWorkItemActivityService { + + @Resource + private PmsWorkItemActivityMapper activityMapper; + + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsWorkItemService workItemService; + @Resource + private PmsWorkItemLabelService workItemLabelService; + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsIterationService iterationService; + + @Resource + private AdminUserApi adminUserApi; + + @Override + public void createWorkItemUpdateActivities(PmsWorkItemDO oldWorkItem, PmsWorkItemDO newWorkItem, + Collection oldMemberUserIds, Collection newMemberUserIds, + Long userId) { + // 1.1 收集动态展示需要的用户、标签、迭代和关联工作项编号 + Set userIds = new LinkedHashSet<>(); + userIds.addAll(oldMemberUserIds); + userIds.addAll(newMemberUserIds); + userIds.addAll(convertSet(Arrays.asList(oldWorkItem.getAssigneeUserId(), newWorkItem.getAssigneeUserId()))); + Map userMap = adminUserApi.getUserMap(userIds); + Set labelIds = new LinkedHashSet<>(); + labelIds.addAll(convertSet(oldWorkItem.getLabelIds())); + labelIds.addAll(convertSet(newWorkItem.getLabelIds())); + // 1.2 批量查询动态展示需要的关联数据 + Map labelMap = workItemLabelService.getWorkItemLabelMap(labelIds); + Set iterationIds = convertSet(Arrays.asList(oldWorkItem.getIterationId(), newWorkItem.getIterationId())); + Map iterationMap = iterationService.getIterationMap(iterationIds); + Set relatedWorkItemIds = convertSet(Arrays.asList(oldWorkItem.getParentId(), newWorkItem.getParentId(), + oldWorkItem.getRelatedRequirementId(), newWorkItem.getRelatedRequirementId())); + Map relatedWorkItemMap = convertMap(workItemService.getWorkItemList(relatedWorkItemIds), + PmsWorkItemDO::getId); + + // 2.1 记录每个发生变化的字段,便于动态列表准确还原修改内容 + createWorkItemActivityIfChanged(oldWorkItem, userId, PmsWorkItemActivityContentEnum.WORK_ITEM_NAME_UPDATED, + oldWorkItem.getName(), newWorkItem.getName(), oldWorkItem.getName(), newWorkItem.getName()); + if (ObjectUtil.notEqual(oldWorkItem.getDescription(), newWorkItem.getDescription())) { + createWorkItemActivity(oldWorkItem.getProjectId(), oldWorkItem.getId(), userId, + PmsWorkItemActivityContentEnum.WORK_ITEM_DESCRIPTION_UPDATED); + } + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "优先级", + oldWorkItem.getPriority(), newWorkItem.getPriority(), + getPriorityName(oldWorkItem.getPriority()), getPriorityName(newWorkItem.getPriority())); + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "负责人", + oldWorkItem.getAssigneeUserId(), newWorkItem.getAssigneeUserId(), + getActivityValue(oldWorkItem.getAssigneeUserId(), userMap, AdminUserRespDTO::getNickname), + getActivityValue(newWorkItem.getAssigneeUserId(), userMap, AdminUserRespDTO::getNickname)); + createWorkItemActivityIfChanged(oldWorkItem, userId, PmsWorkItemActivityContentEnum.WORK_ITEM_MEMBERS_UPDATED, + oldMemberUserIds, newMemberUserIds, + getActivityValues(oldMemberUserIds, userMap, AdminUserRespDTO::getNickname), + getActivityValues(newMemberUserIds, userMap, AdminUserRespDTO::getNickname)); + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "所属迭代", + oldWorkItem.getIterationId(), newWorkItem.getIterationId(), + getActivityValue(oldWorkItem.getIterationId(), iterationMap, PmsIterationDO::getName), + getActivityValue(newWorkItem.getIterationId(), iterationMap, PmsIterationDO::getName)); + createWorkItemActivityIfChanged(oldWorkItem, userId, PmsWorkItemActivityContentEnum.WORK_ITEM_PARENT_UPDATED, + oldWorkItem.getParentId(), newWorkItem.getParentId(), + getActivityValue(oldWorkItem.getParentId(), relatedWorkItemMap, PmsWorkItemDO::getName), + getActivityValue(newWorkItem.getParentId(), relatedWorkItemMap, PmsWorkItemDO::getName)); + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "关联需求", + oldWorkItem.getRelatedRequirementId(), newWorkItem.getRelatedRequirementId(), + getActivityValue(oldWorkItem.getRelatedRequirementId(), relatedWorkItemMap, PmsWorkItemDO::getName), + getActivityValue(newWorkItem.getRelatedRequirementId(), relatedWorkItemMap, PmsWorkItemDO::getName)); + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "缺陷类型", + oldWorkItem.getDefectType(), newWorkItem.getDefectType(), + getDefectTypeName(oldWorkItem.getDefectType()), getDefectTypeName(newWorkItem.getDefectType())); + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "开始时间", + oldWorkItem.getStartTime(), newWorkItem.getStartTime(), + formatActivityTime(oldWorkItem.getStartTime()), formatActivityTime(newWorkItem.getStartTime())); + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "截止时间", + oldWorkItem.getEndTime(), newWorkItem.getEndTime(), + formatActivityTime(oldWorkItem.getEndTime()), formatActivityTime(newWorkItem.getEndTime())); + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "预估工时", + oldWorkItem.getEstimatedHours(), newWorkItem.getEstimatedHours(), + formatActivityValue(oldWorkItem.getEstimatedHours()), formatActivityValue(newWorkItem.getEstimatedHours())); + createWorkItemFieldActivityIfChanged(oldWorkItem, userId, "进度", + oldWorkItem.getProgress(), newWorkItem.getProgress(), + formatActivityValue(oldWorkItem.getProgress()), formatActivityValue(newWorkItem.getProgress())); + createWorkItemActivityIfChanged(oldWorkItem, userId, PmsWorkItemActivityContentEnum.WORK_ITEM_LABELS_UPDATED, + convertSet(oldWorkItem.getLabelIds()), convertSet(newWorkItem.getLabelIds()), + getActivityValues(oldWorkItem.getLabelIds(), labelMap, PmsWorkItemLabelDO::getName), + getActivityValues(newWorkItem.getLabelIds(), labelMap, PmsWorkItemLabelDO::getName)); + createWorkItemActivityIfChanged(oldWorkItem, userId, PmsWorkItemActivityContentEnum.WORK_ITEM_ATTACHMENTS_UPDATED, + oldWorkItem.getFileUrls(), newWorkItem.getFileUrls(), + getActivityFileNames(oldWorkItem.getFileUrls()), getActivityFileNames(newWorkItem.getFileUrls())); + } + + @Override + public void createWorkItemFieldActivityIfChanged(PmsWorkItemDO workItem, Long userId, String fieldName, + Object oldFieldValue, Object newFieldValue, + String oldValue, String newValue) { + if (Objects.equals(oldFieldValue, newFieldValue)) { + return; + } + createWorkItemActivity(workItem.getProjectId(), workItem.getId(), userId, + PmsWorkItemActivityContentEnum.WORK_ITEM_FIELD_UPDATED, fieldName, oldValue, newValue); + } + + @Override + public void createWorkItemIterationActivityIfChanged(PmsWorkItemDO workItem, Long userId, + Long oldIterationId, Long newIterationId) { + Map iterationMap = iterationService.getIterationMap( + convertSet(Arrays.asList(oldIterationId, newIterationId))); + createWorkItemFieldActivityIfChanged(workItem, userId, "所属迭代", oldIterationId, newIterationId, + getActivityValue(oldIterationId, iterationMap, PmsIterationDO::getName), + getActivityValue(newIterationId, iterationMap, PmsIterationDO::getName)); + } + + private void createWorkItemActivityIfChanged(PmsWorkItemDO workItem, Long userId, + PmsWorkItemActivityContentEnum content, + Object oldFieldValue, Object newFieldValue, + String oldValue, String newValue) { + if (Objects.equals(oldFieldValue, newFieldValue)) { + return; + } + createWorkItemActivity(workItem.getProjectId(), workItem.getId(), userId, content, oldValue, newValue); + } + + private static String getPriorityName(Integer priority) { + PmsWorkItemPriorityEnum priorityEnum = PmsWorkItemPriorityEnum.valueOf(priority); + return priorityEnum == null ? formatActivityValue(priority) : priorityEnum.getName(); + } + + private static String getDefectTypeName(Integer defectType) { + PmsWorkItemDefectTypeEnum defectTypeEnum = PmsWorkItemDefectTypeEnum.valueOf(defectType); + return defectTypeEnum == null ? formatActivityValue(defectType) : defectTypeEnum.getName(); + } + + private static String formatActivityTime(LocalDateTime time) { + return time == null ? "无" : time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + } + + private static String formatActivityValue(Object value) { + return value == null ? "无" : String.valueOf(value); + } + + private static String getActivityValue(Long id, Map itemMap, Function nameFunction) { + if (id == null) { + return "无"; + } + T item = itemMap.get(id); + return item == null ? "#" + id : nameFunction.apply(item); + } + + private static String getActivityValues(Collection ids, Map itemMap, + Function nameFunction) { + if (CollUtil.isEmpty(ids)) { + return "无"; + } + return CollUtil.join(convertList(ids, id -> getActivityValue(id, itemMap, nameFunction)), "、"); + } + + private static String getActivityFileNames(List fileUrls) { + if (CollUtil.isEmpty(fileUrls)) { + return "无"; + } + return CollUtil.join(convertList(fileUrls, fileUrl -> StrUtil.subAfter(fileUrl, '/', true)), "、"); + } + + @Override + public void createWorkItemActivity(Long projectId, Long workItemId, Long operatorUserId, + PmsWorkItemActivityContentEnum content, Object... arguments) { + activityMapper.insert(new PmsWorkItemActivityDO().setProjectId(projectId).setWorkItemId(workItemId) + .setOperatorUserId(operatorUserId).setContent(content.format(arguments))); + } + + @Override + public List getWorkItemActivityList(Long workItemId) { + return activityMapper.selectListByWorkItemId(workItemId); + } + + @Override + public List getWorkItemActivityListByWorkItemIds(Collection workItemIds, int limit) { + if (CollUtil.isEmpty(workItemIds)) { + return Collections.emptyList(); + } + return activityMapper.selectListByWorkItemIds(workItemIds, limit); + } + + @Override + public void deleteWorkItemActivityListByWorkItemId(Long workItemId) { + activityMapper.deleteByWorkItemId(workItemId); + } + + @Override + public void deleteWorkItemActivityListByProjectId(Long projectId) { + activityMapper.deleteByProjectId(projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemBoardService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemBoardService.java new file mode 100644 index 000000000..f64087eb5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemBoardService.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 工作项看板列 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkItemBoardService { + + /** + * 获得项目指定工作项类型的看板列 + * + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @return 看板列列表 + */ + List getWorkItemBoardList(Long projectId, Integer workItemType); + + /** + * 创建看板列 + * + * @param board 看板列 + */ + void createWorkItemBoard(PmsWorkItemBoardDO board); + + /** + * 批量创建看板列 + * + * @param boards 看板列列表 + */ + void createWorkItemBoardList(Collection boards); + + /** + * 批量更新看板列 + * + * @param boards 看板列列表 + */ + void updateWorkItemBoardList(Collection boards); + + /** + * 删除指定看板列 + * + * @param ids 看板列编号集合 + */ + void deleteWorkItemBoardList(Collection ids); + + /** + * 删除项目的全部看板列 + * + * @param projectId 项目编号 + */ + void deleteWorkItemBoardListByProjectId(Long projectId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemBoardServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemBoardServiceImpl.java new file mode 100644 index 000000000..f022d0aa3 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemBoardServiceImpl.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemBoardMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 工作项看板列 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsWorkItemBoardServiceImpl implements PmsWorkItemBoardService { + + @Resource + private PmsWorkItemBoardMapper workItemBoardMapper; + + @Override + public List getWorkItemBoardList(Long projectId, Integer workItemType) { + return workItemBoardMapper.selectListByProjectIdAndWorkItemType(projectId, workItemType); + } + + @Override + public void createWorkItemBoard(PmsWorkItemBoardDO board) { + workItemBoardMapper.insert(board); + } + + @Override + public void createWorkItemBoardList(Collection boards) { + if (CollUtil.isEmpty(boards)) { + return; + } + workItemBoardMapper.insertBatch(boards); + } + + @Override + public void updateWorkItemBoardList(Collection boards) { + if (CollUtil.isEmpty(boards)) { + return; + } + workItemBoardMapper.updateBatch(boards); + } + + @Override + public void deleteWorkItemBoardList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + workItemBoardMapper.deleteByIds(ids); + } + + @Override + public void deleteWorkItemBoardListByProjectId(Long projectId) { + workItemBoardMapper.deleteByProjectId(projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentService.java new file mode 100644 index 000000000..ee31f298c --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentService.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.comment.PmsWorkItemCommentSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemCommentDO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 工作项评论 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkItemCommentService { + + /** + * 创建工作项评论或回复 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @return 评论编号 + */ + Long createWorkItemComment(PmsWorkItemCommentSaveReqVO saveReqVO, Long userId); + + /** + * 更新自己的工作项评论内容 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + */ + void updateWorkItemComment(PmsWorkItemCommentSaveReqVO saveReqVO, Long userId); + + /** + * 删除自己的工作项评论,删除主评论时同步删除全部回复 + * + * @param id 评论编号 + * @param userId 用户编号 + */ + void deleteWorkItemComment(Long id, Long userId); + + /** + * 删除工作项的全部评论,用于工作项删除时级联清理 + * + * @param workItemId 工作项编号 + */ + void deleteWorkItemCommentListByWorkItemId(Long workItemId); + + /** + * 批量删除工作项的评论,用于项目删除时级联清理 + * + * @param workItemIds 工作项编号列表 + */ + void deleteWorkItemCommentListByWorkItemIds(Collection workItemIds); + + /** + * 获得工作项的评论和回复列表 + * + * @param workItemId 工作项编号 + * @param userId 用户编号 + * @return 评论列表 + */ + List getWorkItemCommentList(Long workItemId, Long userId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentServiceImpl.java new file mode 100644 index 000000000..415bce153 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentServiceImpl.java @@ -0,0 +1,153 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.comment.PmsWorkItemCommentSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemCommentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemCommentMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_COMMENT_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_COMMENT_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_COMMENT_REPLY_INVALID; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.COMMENT_CREATED; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.COMMENT_DELETED; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.COMMENT_REPLIED; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.COMMENT_UPDATED; + +/** + * PMS 工作项评论 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsWorkItemCommentServiceImpl implements PmsWorkItemCommentService { + + @Resource + private PmsWorkItemCommentMapper commentMapper; + + @Resource + private PmsWorkItemService workItemService; + @Resource + private PmsWorkItemActivityService workItemActivityService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createWorkItemComment(PmsWorkItemCommentSaveReqVO saveReqVO, Long userId) { + // 1.1 校验工作项可编辑 + PmsWorkItemDO workItem = workItemService.getWritableWorkItem(saveReqVO.getWorkItemId(), userId); + // 1.2 校验回复关系 + Long mainId = saveReqVO.getMainId() != null ? saveReqVO.getMainId() : PmsWorkItemCommentDO.MAIN_ID_ROOT; + if (ObjectUtil.notEqual(PmsWorkItemCommentDO.MAIN_ID_ROOT, mainId)) { + PmsWorkItemCommentDO mainComment = validateCommentExists(mainId); + if (ObjectUtil.notEqual(saveReqVO.getWorkItemId(), mainComment.getWorkItemId()) + || ObjectUtil.notEqual(PmsWorkItemCommentDO.MAIN_ID_ROOT, mainComment.getMainId())) { + throw exception(WORK_ITEM_COMMENT_REPLY_INVALID); + } + } + + // 2.1 创建评论 + PmsWorkItemCommentDO comment = BeanUtils.toBean(saveReqVO, PmsWorkItemCommentDO.class) + .setUserId(userId).setMainId(mainId); + commentMapper.insert(comment); + // 2.2 记录评论动态 + workItemActivityService.createWorkItemActivity(workItem.getProjectId(), workItem.getId(), userId, + ObjectUtil.equal(PmsWorkItemCommentDO.MAIN_ID_ROOT, mainId) ? COMMENT_CREATED : COMMENT_REPLIED); + return comment.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemComment(PmsWorkItemCommentSaveReqVO saveReqVO, Long userId) { + // 1.1 校验评论存在 + PmsWorkItemCommentDO comment = validateCommentExists(saveReqVO.getId()); + // 1.2 校验工作项可编辑 + PmsWorkItemDO workItem = workItemService.getWritableWorkItem(comment.getWorkItemId(), userId); + // 1.3 校验评论属于当前用户 + validateCommentOwner(comment, userId); + + // 2.1 更新评论内容 + commentMapper.updateById(BeanUtils.toBean(saveReqVO, PmsWorkItemCommentDO.class).setId(comment.getId())); + // 2.2 记录评论更新动态 + workItemActivityService.createWorkItemActivity( + workItem.getProjectId(), workItem.getId(), userId, COMMENT_UPDATED); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteWorkItemComment(Long id, Long userId) { + // 1.1 校验评论存在 + PmsWorkItemCommentDO comment = validateCommentExists(id); + // 1.2 校验工作项可编辑 + PmsWorkItemDO workItem = workItemService.getWritableWorkItem(comment.getWorkItemId(), userId); + // 1.3 校验评论属于当前用户 + validateCommentOwner(comment, userId); + + // 2.1 删除主评论时同步删除全部回复 + if (PmsWorkItemCommentDO.MAIN_ID_ROOT.equals(comment.getMainId())) { + commentMapper.deleteByMainId(comment.getId()); + } + commentMapper.deleteById(comment.getId()); + // 2.2 记录评论删除动态 + workItemActivityService.createWorkItemActivity( + workItem.getProjectId(), workItem.getId(), userId, COMMENT_DELETED); + } + + @Override + public List getWorkItemCommentList(Long workItemId, Long userId) { + // 1. 校验工作项可访问 + workItemService.getWorkItem(workItemId, userId); + + // 2. 查询评论和回复 + return commentMapper.selectListByWorkItemId(workItemId); + } + + /** + * 校验工作项评论存在 + * + * @param id 评论编号 + * @return 工作项评论 + */ + private PmsWorkItemCommentDO validateCommentExists(Long id) { + PmsWorkItemCommentDO comment = commentMapper.selectById(id); + if (comment == null) { + throw exception(WORK_ITEM_COMMENT_NOT_EXISTS); + } + return comment; + } + + /** + * 校验当前用户是评论创建人 + * + * @param comment 工作项评论 + * @param userId 后台用户编号 + */ + private void validateCommentOwner(PmsWorkItemCommentDO comment, Long userId) { + if (ObjectUtil.notEqual(userId, comment.getUserId())) { + throw exception(WORK_ITEM_COMMENT_ACCESS_DENIED); + } + } + + @Override + public void deleteWorkItemCommentListByWorkItemId(Long workItemId) { + commentMapper.deleteByWorkItemId(workItemId); + } + + @Override + public void deleteWorkItemCommentListByWorkItemIds(Collection workItemIds) { + if (CollUtil.isEmpty(workItemIds)) { + return; + } + commentMapper.deleteByWorkItemIds(workItemIds); + } +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelService.java new file mode 100644 index 000000000..b0f5c2742 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelService.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label.PmsWorkItemLabelSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * PMS 工作项标签 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkItemLabelService { + + /** + * 创建工作项标签 + * + * @param saveReqVO 保存信息 + * @return 标签编号 + */ + Long createWorkItemLabel(PmsWorkItemLabelSaveReqVO saveReqVO); + + /** + * 更新工作项标签 + * + * @param saveReqVO 保存信息 + */ + void updateWorkItemLabel(PmsWorkItemLabelSaveReqVO saveReqVO); + + /** + * 删除工作项标签,工作项中的历史编号在展示时自动忽略 + * + * @param id 标签编号 + */ + void deleteWorkItemLabel(Long id); + + /** + * 获得匹配名称的工作项标签列表 + * + * @param name 标签名称,模糊匹配 + * @return 标签列表 + */ + List getWorkItemLabelList(String name); + + /** + * 获得工作项标签列表 + * + * @param ids 标签编号集合 + * @return 标签列表 + */ + List getWorkItemLabelList(Collection ids); + + /** + * 获得工作项标签 Map + * + * @param ids 标签编号集合 + * @return 工作项标签 Map + */ + default Map getWorkItemLabelMap(Collection ids) { + return convertMap(getWorkItemLabelList(ids), PmsWorkItemLabelDO::getId); + } + + /** + * 校验工作项标签编号都存在 + * + * @param ids 标签编号集合 + */ + void validateWorkItemLabelIds(Collection ids); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelServiceImpl.java new file mode 100644 index 000000000..4e8b70e79 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelServiceImpl.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label.PmsWorkItemLabelSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemLabelMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_LABELS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_LABEL_NOT_EXISTS; + +/** + * PMS 工作项标签 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsWorkItemLabelServiceImpl implements PmsWorkItemLabelService { + + @Resource + private PmsWorkItemLabelMapper labelMapper; + + @Override + public Long createWorkItemLabel(PmsWorkItemLabelSaveReqVO saveReqVO) { + PmsWorkItemLabelDO label = BeanUtils.toBean(saveReqVO, PmsWorkItemLabelDO.class) + .setName(StrUtil.trim(saveReqVO.getName())).setColor(StrUtil.trim(saveReqVO.getColor())); + labelMapper.insert(label); + return label.getId(); + } + + @Override + public void updateWorkItemLabel(PmsWorkItemLabelSaveReqVO saveReqVO) { + // 1. 校验标签存在 + validateLabelExists(saveReqVO.getId()); + + // 2. 更新标签 + labelMapper.updateById(BeanUtils.toBean(saveReqVO, PmsWorkItemLabelDO.class) + .setName(StrUtil.trim(saveReqVO.getName())).setColor(StrUtil.trim(saveReqVO.getColor()))); + } + + @Override + public void deleteWorkItemLabel(Long id) { + // 1. 校验标签存在 + validateLabelExists(id); + + // 2. 删除标签;工作项中的历史编号在展示时自动忽略 + labelMapper.deleteById(id); + } + + @Override + public List getWorkItemLabelList(String name) { + return labelMapper.selectListByName(name); + } + + @Override + public List getWorkItemLabelList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return labelMapper.selectByIds(ids); + } + + @Override + public void validateWorkItemLabelIds(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + if (labelMapper.selectByIds(ids).size() != ids.size()) { + throw exception(WORK_ITEM_LABELS_INVALID); + } + } + + /** + * 校验工作项标签存在 + * + * @param id 标签编号 + */ + private void validateLabelExists(Long id) { + if (labelMapper.selectById(id) == null) { + throw exception(WORK_ITEM_LABEL_NOT_EXISTS); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemService.java new file mode 100644 index 000000000..d26248d84 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemService.java @@ -0,0 +1,296 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemBoardReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemBoardRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemImportExcelVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemImportRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemIterationUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemNameUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPlanningSortReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * PMS 工作项 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkItemService { + + /** + * 创建工作项 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @return 工作项编号 + */ + Long createWorkItem(PmsWorkItemSaveReqVO saveReqVO, Long userId); + + /** + * 更新工作项 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + */ + void updateWorkItem(PmsWorkItemSaveReqVO saveReqVO, Long userId); + + /** + * 更新工作项名称 + * + * @param updateReqVO 名称更新信息 + * @param userId 用户编号 + */ + void updateWorkItemName(PmsWorkItemNameUpdateReqVO updateReqVO, Long userId); + + /** + * 更新工作项状态 + * + * @param updateReqVO 状态更新信息 + * @param userId 用户编号 + */ + void updateWorkItemStatus(PmsWorkItemStatusUpdateReqVO updateReqVO, Long userId); + + /** + * 更新指定状态下全部工作项的语义状态 + * + * @param statusId 状态编号 + * @param statusType 语义状态 + */ + void updateWorkItemStatusTypeByStatusId(Long statusId, Integer statusType); + + /** + * 获得指定状态下的工作项数量 + * + * @param statusId 状态编号 + * @return 工作项数量 + */ + Long getWorkItemCountByStatusId(Long statusId); + + /** + * 将指定状态下的工作项迁移到目标状态 + * + * @param sourceStatusId 原状态编号 + * @param targetStatusId 目标状态编号 + * @param targetStatus 目标语义状态 + */ + void transferWorkItemStatus(Long sourceStatusId, Long targetStatusId, Integer targetStatus); + + /** + * 规划工作项到迭代,或移回待规划 + * + * @param updateReqVO 所属迭代更新信息 + * @param userId 用户编号 + */ + void updateWorkItemIteration(PmsWorkItemIterationUpdateReqVO updateReqVO, Long userId); + + /** + * 更新看板列内工作项顺序 + * + * @param sortReqVO 排序信息 + * @param userId 用户编号 + */ + void updateWorkItemSort(PmsWorkItemSortReqVO sortReqVO, Long userId); + + /** + * 更新待规划或迭代内工作项顺序 + * + * @param sortReqVO 排序信息 + * @param userId 用户编号 + */ + void updateWorkItemPlanningSort(PmsWorkItemPlanningSortReqVO sortReqVO, Long userId); + + /** + * 归档工作项 + * + * @param id 工作项编号 + * @param userId 用户编号 + */ + void archiveWorkItem(Long id, Long userId); + + /** + * 将工作项移入回收站 + * + * @param id 工作项编号 + * @param userId 用户编号 + */ + void recycleWorkItem(Long id, Long userId); + + /** + * 恢复工作项 + * + * @param id 工作项编号 + * @param userId 用户编号 + */ + void restoreWorkItem(Long id, Long userId); + + /** + * 彻底删除回收站中的工作项 + * + * @param id 工作项编号 + * @param userId 用户编号 + */ + void deleteWorkItem(Long id, Long userId); + + /** + * 删除项目的全部工作项、参与人及状态 + * + * @param projectId 项目编号 + */ + void deleteWorkItemListByProjectId(Long projectId); + + /** + * 清空工作项的所属迭代 + * + * @param iterationId 迭代编号 + */ + void clearWorkItemIterationId(Long iterationId); + + /** + * 获得工作项 + * + * @param id 工作项编号 + * @param userId 用户编号 + * @return 工作项 + */ + PmsWorkItemDO getWorkItem(Long id, Long userId); + + /** + * 获得可编辑的正常工作项 + * + * @param id 工作项编号 + * @param userId 用户编号 + * @return 工作项 + */ + PmsWorkItemDO getWritableWorkItem(Long id, Long userId); + + /** + * 获得工作项分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 工作项分页 + */ + PageResult getWorkItemPage(PmsWorkItemPageReqVO pageReqVO, Long userId); + + /** + * 获得项目工作项语义状态数量 Map + * + * @param projectIds 项目编号集合 + * @return 项目编号到状态数量 Map + */ + Map> getProjectWorkItemStatusCountMap(Collection projectIds); + + /** + * 获得工作项列表 + * + * @param pageReqVO 查询条件 + * @param userId 用户编号 + * @return 工作项列表 + */ + List getWorkItemList(PmsWorkItemPageReqVO pageReqVO, Long userId); + + /** + * 获得项目的正常工作项列表 + * + * @param projectId 项目编号 + * @return 工作项列表 + */ + List getActiveWorkItemListByProjectId(Long projectId); + + /** + * 获得迭代的正常工作项列表 + * + * @param iterationId 迭代编号 + * @return 工作项列表 + */ + List getActiveWorkItemListByIterationId(Long iterationId); + + /** + * 获得迭代工作项语义状态数量 Map + * + * @param iterationIds 迭代编号集合 + * @return 迭代编号到状态数量 Map + */ + Map> getIterationWorkItemStatusCountMap(Collection iterationIds); + + /** + * 获得分配给用户的未完成工作项分页 + * + * @param pageReqVO 工作台分页条件 + * @param projectIds 可访问项目编号集合 + * @param userId 用户编号 + * @return 工作项分页 + */ + PageResult getAssignedWorkItemPage(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long userId); + + /** + * 获得分配给用户的未完成工作项类型数量 Map + * + * @param pageReqVO 工作台查询条件 + * @param projectIds 可访问项目编号集合 + * @param userId 用户编号 + * @return 工作项类型与数量 Map + */ + Map getAssignedWorkItemTypeCountMap(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long userId); + + /** + * 导入工作项 + * + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @param importRows 导入数据 + * @param userId 用户编号 + * @return 导入结果 + */ + PmsWorkItemImportRespVO importWorkItemList(Long projectId, Integer workItemType, + List importRows, Long userId); + + /** + * 获得工作项看板 + * + * @param queryReqVO 看板查询 + * @param userId 用户编号 + * @return 看板列列表 + */ + List getWorkItemBoard(PmsWorkItemBoardReqVO queryReqVO, Long userId); + + /** + * 获得工作项参与人用户编号 Map + * + * @param workItemIds 工作项编号集合 + * @return 工作项参与人用户编号 Map + */ + Map> getWorkItemMemberUserIdListMap(Collection workItemIds); + + /** + * 获得指定编号的工作项列表 + * + * @param ids 工作项编号集合 + * @return 工作项列表 + */ + List getWorkItemList(Collection ids); + + /** + * 获得工作项 Map + * + * @param ids 工作项编号集合 + * @return 工作项 Map + */ + default Map getWorkItemMap(Collection ids) { + return convertMap(getWorkItemList(ids), PmsWorkItemDO::getId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemServiceImpl.java new file mode 100644 index 000000000..73825968d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemServiceImpl.java @@ -0,0 +1,955 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemBoardReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemBoardRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemImportExcelVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemImportRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemIterationUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemNameUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPlanningSortReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemSortReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemMemberDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemMemberMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemLifecycleStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.google.common.collect.Maps; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_DEFECT_TYPE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_DELETE_STATUS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_ITERATION_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_LIFECYCLE_STATUS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_PARENT_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_RELATED_REQUIREMENT_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_SORT_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_TYPE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.MessageTemplateConstants.WORK_ITEM_ASSIGNED; + +/** + * PMS 工作项 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class PmsWorkItemServiceImpl implements PmsWorkItemService { + + @Resource + private PmsWorkItemMapper workItemMapper; + + @Resource + private PmsWorkItemMemberMapper workItemMemberMapper; + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsWorkItemCommentService workItemCommentService; + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsWorkItemWorkLogService workItemWorkLogService; + @Resource + private PmsWorkItemUserSortService workItemUserSortService; + @Resource + private PmsProjectMemberService projectMemberService; + @Resource + private PmsIterationService iterationService; + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsWorkItemStatusService workItemStatusService; + @Resource + private PmsWorkItemLabelService workItemLabelService; + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsWorkItemActivityService workItemActivityService; + + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + @Resource + private AdminUserApi adminUserApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createWorkItem(PmsWorkItemSaveReqVO saveReqVO, Long userId) { + // 1.1 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(saveReqVO.getProjectId(), userId); + // 1.2 校验项目处于进行中 + validateActiveProject(project); + // 1.3 校验工作项类型可用 + validateWorkItemType(project, saveReqVO.getType()); + // 1.4 校验工作项关联关系 + validateWorkItemRelations(null, project, saveReqVO.getType(), saveReqVO, userId); + // 1.5 校验工作项参与人 + validateWorkItemMembers(project.getId(), saveReqVO, userId); + // 1.6 校验工作项标签 + workItemLabelService.validateWorkItemLabelIds(saveReqVO.getLabelIds()); + + // 2. 创建工作项,生命周期初始状态由后端控制 + PmsWorkItemStatusDO defaultStatus = workItemStatusService + .getDefaultWorkItemStatus(project.getId(), saveReqVO.getType()); + Integer maxSerialNumber = workItemMapper.selectMaxSerialNumberByProjectId(project.getId()); + Integer maxSort = workItemMapper.selectMaxSortByStatusId(defaultStatus.getId()); + PmsWorkItemDO workItem = BeanUtils.toBean(saveReqVO, PmsWorkItemDO.class) + .setName(saveReqVO.getName().trim()) + .setSerialNumber(maxSerialNumber == null ? 1 : maxSerialNumber + 1) + .setStatusId(defaultStatus.getId()).setStatus(defaultStatus.getStatusType()) + .setLifecycleStatus(PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .setSort(maxSort == null ? 1 : maxSort + 1); + workItemMapper.insert(workItem); + + // 3.1 保存工作项参与人 + saveWorkItemMembers(workItem.getId(), project.getId(), getMemberUserIds(saveReqVO, userId)); + // 3.2 记录工作项创建动态 + workItemActivityService.createWorkItemActivity( + project.getId(), workItem.getId(), userId, PmsWorkItemActivityContentEnum.WORK_ITEM_CREATED); + + // 4.1 创建内联子工作项,沿用父项类型、迭代和看板状态 + List childWorkItemNames = saveReqVO.getChildWorkItemNames(); + if (CollUtil.isNotEmpty(childWorkItemNames)) { + for (String childWorkItemName : childWorkItemNames) { + PmsWorkItemSaveReqVO childSaveReqVO = new PmsWorkItemSaveReqVO().setProjectId(project.getId()) + .setType(workItem.getType()).setName(childWorkItemName) + .setPriority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()).setProgress(0) + .setIterationId(workItem.getIterationId()).setParentId(workItem.getId()); + Long childWorkItemId = createWorkItem(childSaveReqVO, userId); + // 子工作项创建后同步父项状态和排序,保持看板中的位置一致 + workItemMapper.updateById(new PmsWorkItemDO().setId(childWorkItemId) + .setStatusId(workItem.getStatusId()).setStatus(workItem.getStatus()).setSort(workItem.getSort())); + } + } + // 4.2 创建时登记工时 + if (saveReqVO.getActualHours() != null) { + workItemWorkLogService.createWorkItemWorkLog(new PmsWorkItemWorkLogSaveReqVO() + .setWorkItemId(workItem.getId()).setActualHours(saveReqVO.getActualHours()) + .setRemainingHours(saveReqVO.getRemainingHours()), userId); + } + + // 5. 通知工作项负责人 + sendWorkItemAssignedNotify(workItem.getAssigneeUserId(), userId, project.getId(), + project.getName(), workItem.getSerialNumber(), workItem.getName()); + return workItem.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItem(PmsWorkItemSaveReqVO saveReqVO, Long userId) { + // 1.1 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(saveReqVO.getId()); + // 1.2 校验工作项处于正常状态 + validateActiveWorkItem(workItem); + // 1.3 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(workItem.getProjectId(), userId); + // 1.4 校验项目处于进行中 + validateActiveProject(project); + // 1.5 校验工作项关联关系 + validateWorkItemRelations(workItem.getId(), project, workItem.getType(), saveReqVO, userId); + // 1.6 校验工作项参与人 + validateWorkItemMembers(project.getId(), saveReqVO, userId); + // 1.7 校验工作项标签 + workItemLabelService.validateWorkItemLabelIds(saveReqVO.getLabelIds()); + Set oldMemberUserIds = convertSet(workItemMemberMapper.selectListByWorkItemIds( + Collections.singleton(workItem.getId())), PmsWorkItemMemberDO::getUserId); + Set newMemberUserIds = getMemberUserIds(saveReqVO, userId); + + // 2. 更新工作项业务字段,不允许表单修改类型和生命周期状态 + PmsWorkItemDO updateObj = BeanUtils.toBean(saveReqVO, PmsWorkItemDO.class) + .setProjectId(null).setType(null).setName(saveReqVO.getName().trim()); + workItemMapper.updateForEdit(updateObj); + + // 3.1 重建工作项参与人关系 + workItemMemberMapper.deleteByWorkItemId(workItem.getId()); + saveWorkItemMembers(workItem.getId(), project.getId(), newMemberUserIds); + // 3.2 按字段记录工作项更新动态 + workItemActivityService.createWorkItemUpdateActivities(workItem, updateObj, + oldMemberUserIds, newMemberUserIds, userId); + + // 4. 负责人发生变化时通知新负责人 + if (ObjectUtil.notEqual(workItem.getAssigneeUserId(), saveReqVO.getAssigneeUserId())) { + sendWorkItemAssignedNotify(saveReqVO.getAssigneeUserId(), userId, project.getId(), + project.getName(), workItem.getSerialNumber(), updateObj.getName()); + } + } + + /** + * 发送工作项指派站内信。通知配置异常不能阻断工作项主业务 + */ + private void sendWorkItemAssignedNotify(Long receiverUserId, Long operatorUserId, Long projectId, + String projectName, Integer serialNumber, String workItemName) { + if (receiverUserId == null || receiverUserId.equals(operatorUserId)) { + return; + } + Map templateParams = Maps.newHashMapWithExpectedSize(4); + templateParams.put("projectName", projectName); + templateParams.put("serialNumber", serialNumber); + templateParams.put("workItemName", workItemName); + // 工作项通知跳转到项目详情的任务页,与前端项目详情路由及 tabs 参数保持一致 + templateParams.put("route", "/pms/pm/project/detail/" + projectId + "?tabs=task"); + try { + notifyMessageSendApi.sendSingleMessageToAdmin(new NotifySendSingleToUserReqDTO() + .setUserId(receiverUserId).setTemplateCode(WORK_ITEM_ASSIGNED) + .setTemplateParams(templateParams)).checkError(); + } catch (RuntimeException ex) { + log.warn("[sendWorkItemAssignedNotify][向用户({})发送工作项指派站内信失败,参数为({})]", + receiverUserId, templateParams, ex); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemName(PmsWorkItemNameUpdateReqVO updateReqVO, Long userId) { + // 1. 校验工作项可编辑 + PmsWorkItemDO workItem = getWritableWorkItem(updateReqVO.getId(), userId); + + // 2.1 更新工作项名称 + String name = updateReqVO.getName().trim(); + workItemMapper.updateById(BeanUtils.toBean(updateReqVO, PmsWorkItemDO.class) + .setId(workItem.getId()).setName(name)); + // 2.2 记录工作项名称更新动态 + workItemActivityService.createWorkItemActivity( + workItem.getProjectId(), workItem.getId(), userId, PmsWorkItemActivityContentEnum.WORK_ITEM_NAME_UPDATED, + workItem.getName(), name); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemStatus(PmsWorkItemStatusUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(updateReqVO.getId()); + // 1.2 校验工作项处于正常状态 + validateActiveWorkItem(workItem); + // 1.3 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(workItem.getProjectId(), userId); + // 1.4 校验项目处于进行中 + validateActiveProject(project); + // 1.5 校验目标状态属于当前项目和工作项类型 + PmsWorkItemStatusDO status = workItemStatusService.validateWorkItemStatus(updateReqVO.getStatusId(), + workItem.getProjectId(), workItem.getType()); + + // 2.1 更新看板状态、语义状态和显示顺序 + Integer maxSort = workItemMapper.selectMaxSortByStatusId(status.getId()); + workItemMapper.updateStatusAndSortById(workItem.getId(), status.getId(), status.getStatusType(), + maxSort == null ? 1 : maxSort + 1); + // 2.2 记录工作项状态更新动态 + workItemActivityService.createWorkItemActivity( + project.getId(), workItem.getId(), userId, PmsWorkItemActivityContentEnum.WORK_ITEM_STATUS_UPDATED, + workItemStatusService.getWorkItemStatus(workItem.getStatusId()).getName(), status.getName()); + } + + @Override + public void updateWorkItemStatusTypeByStatusId(Long statusId, Integer statusType) { + workItemMapper.updateStatusTypeByStatusId(statusId, statusType); + } + + @Override + public Long getWorkItemCountByStatusId(Long statusId) { + return workItemMapper.selectCountByStatusId(statusId); + } + + @Override + public void transferWorkItemStatus(Long sourceStatusId, Long targetStatusId, Integer targetStatus) { + workItemMapper.updateStatusBySourceStatusId(sourceStatusId, targetStatusId, targetStatus); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemIteration(PmsWorkItemIterationUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(updateReqVO.getId()); + // 1.2 校验工作项处于正常状态 + validateActiveWorkItem(workItem); + // 1.3 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(workItem.getProjectId(), userId); + // 1.4 校验项目处于进行中 + validateActiveProject(project); + // 1.5 校验项目支持迭代 + if (ObjectUtil.notEqual(PmsProjectTypeEnum.AGILE.getType(), project.getType())) { + throw exception(WORK_ITEM_ITERATION_INVALID); + } + // 1.6 校验目标迭代属于同一项目 + PmsIterationDO iteration = null; + if (updateReqVO.getIterationId() != null) { + iteration = iterationService.getIteration(updateReqVO.getIterationId(), userId); + if (ObjectUtil.notEqual(project.getId(), iteration.getProjectId())) { + throw exception(WORK_ITEM_ITERATION_INVALID); + } + } + + // 2.1 更新所属迭代 + workItemMapper.updateIterationIdById(workItem.getId(), updateReqVO.getIterationId()); + // 2.2 记录工作项迭代更新动态 + workItemActivityService.createWorkItemIterationActivityIfChanged(workItem, userId, + workItem.getIterationId(), updateReqVO.getIterationId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemSort(PmsWorkItemSortReqVO sortReqVO, Long userId) { + // 1.1 校验目标状态存在 + PmsWorkItemStatusDO status = workItemStatusService.getWorkItemStatus(sortReqVO.getStatusId()); + // 1.2 校验目标状态所属项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(status.getProjectId(), userId); + // 1.3 校验项目处于进行中 + validateActiveProject(project); + + // 2.1 校验排序列表中的工作项编号不重复 + Set workItemIds = new LinkedHashSet<>(sortReqVO.getWorkItemIds()); + List workItems = workItemMapper.selectListByStatusId(status.getId()); + if (workItemIds.size() != sortReqVO.getWorkItemIds().size()) { + throw exception(WORK_ITEM_SORT_INVALID); + } + // 2.2 校验排序列表包含目标状态下的全部工作项 + if (workItems.size() != workItemIds.size() + || ObjectUtil.notEqual(workItemIds, convertSet(workItems, PmsWorkItemDO::getId))) { + throw exception(WORK_ITEM_SORT_INVALID); + } + for (PmsWorkItemDO workItem : workItems) { + // 2.3 校验工作项属于目标项目 + if (ObjectUtil.notEqual(status.getProjectId(), workItem.getProjectId())) { + throw exception(WORK_ITEM_SORT_INVALID); + } + // 2.4 校验工作项类型与目标状态一致 + if (ObjectUtil.notEqual(status.getWorkItemType(), workItem.getType())) { + throw exception(WORK_ITEM_SORT_INVALID); + } + // 2.5 校验工作项属于目标状态 + if (ObjectUtil.notEqual(status.getId(), workItem.getStatusId())) { + throw exception(WORK_ITEM_SORT_INVALID); + } + } + + // 3. 按请求顺序更新列内显示顺序 + Map sortMap = new LinkedHashMap<>(); + for (int index = 0; index < sortReqVO.getWorkItemIds().size(); index++) { + sortMap.put(sortReqVO.getWorkItemIds().get(index), index + 1); + } + List sortedWorkItemIds = new ArrayList<>(workItemIds); + Collections.sort(sortedWorkItemIds); + workItemMapper.updateBatch(convertList(sortedWorkItemIds, + workItemId -> new PmsWorkItemDO().setId(workItemId).setSort(sortMap.get(workItemId)))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemPlanningSort(PmsWorkItemPlanningSortReqVO sortReqVO, Long userId) { + // 1.1 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(sortReqVO.getProjectId(), userId); + // 1.2 校验项目处于进行中 + validateActiveProject(project); + // 1.3 校验目标迭代属于当前项目 + if (sortReqVO.getIterationId() != null) { + PmsIterationDO iteration = iterationService.getIteration(sortReqVO.getIterationId(), userId); + if (ObjectUtil.notEqual(project.getId(), iteration.getProjectId())) { + throw exception(WORK_ITEM_SORT_INVALID); + } + } + + // 1.4 校验请求包含目标区域内的全部一级工作项 + Set workItemIds = new LinkedHashSet<>(sortReqVO.getWorkItemIds()); + List workItems = workItemMapper.selectPlanningList( + sortReqVO.getProjectId(), sortReqVO.getIterationId()); + if (workItemIds.size() != sortReqVO.getWorkItemIds().size() || workItems.size() != workItemIds.size() + || ObjectUtil.notEqual(workItemIds, convertSet(workItems, PmsWorkItemDO::getId))) { + throw exception(WORK_ITEM_SORT_INVALID); + } + + // 3. Backlog 按当前用户保存个人顺序 + if (sortReqVO.getIterationId() == null) { + workItemUserSortService.updateWorkItemUserSort( + sortReqVO.getProjectId(), sortReqVO.getWorkItemIds(), userId); + return; + } + + // 4. 迭代内顺序由项目成员共享 + Map sortMap = new LinkedHashMap<>(); + for (int index = 0; index < sortReqVO.getWorkItemIds().size(); index++) { + sortMap.put(sortReqVO.getWorkItemIds().get(index), index + 1); + } + List sortedWorkItemIds = new ArrayList<>(workItemIds); + Collections.sort(sortedWorkItemIds); + workItemMapper.updateBatch(convertList(sortedWorkItemIds, + workItemId -> new PmsWorkItemDO().setId(workItemId).setSort(sortMap.get(workItemId)))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void archiveWorkItem(Long id, Long userId) { + // 1.1 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(id); + // 1.2 校验工作项处于正常状态 + validateActiveWorkItem(workItem); + // 1.3 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(workItem.getProjectId(), userId); + // 1.4 校验项目处于进行中 + validateActiveProject(project); + + // 2.1 归档工作项 + workItemMapper.updateLifecycleStatusById(id, PmsWorkItemLifecycleStatusEnum.ARCHIVED.getStatus(), + LocalDateTime.now(), null); + // 2.2 记录工作项归档动态 + workItemActivityService.createWorkItemActivity( + project.getId(), workItem.getId(), userId, PmsWorkItemActivityContentEnum.WORK_ITEM_ARCHIVED); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void recycleWorkItem(Long id, Long userId) { + // 1.1 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(id); + // 1.2 校验工作项不在回收站 + if (PmsWorkItemLifecycleStatusEnum.RECYCLED.getStatus().equals(workItem.getLifecycleStatus())) { + throw exception(WORK_ITEM_LIFECYCLE_STATUS_INVALID); + } + // 1.3 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(workItem.getProjectId(), userId); + // 1.4 校验项目处于进行中 + validateActiveProject(project); + + // 2.1 将工作项移入回收站 + workItemMapper.updateLifecycleStatusById(id, PmsWorkItemLifecycleStatusEnum.RECYCLED.getStatus(), + workItem.getArchiveTime(), LocalDateTime.now()); + // 2.2 记录工作项回收动态 + workItemActivityService.createWorkItemActivity( + project.getId(), workItem.getId(), userId, PmsWorkItemActivityContentEnum.WORK_ITEM_RECYCLED); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void restoreWorkItem(Long id, Long userId) { + // 1.1 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(id); + // 1.2 校验工作项已归档或位于回收站 + if (PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus().equals(workItem.getLifecycleStatus())) { + throw exception(WORK_ITEM_LIFECYCLE_STATUS_INVALID); + } + // 1.3 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(workItem.getProjectId(), userId); + // 1.4 校验项目处于进行中 + validateActiveProject(project); + + // 2.1 恢复工作项 + workItemMapper.updateLifecycleStatusById( + id, PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus(), null, null); + // 2.2 记录工作项恢复动态 + workItemActivityService.createWorkItemActivity( + project.getId(), workItem.getId(), userId, PmsWorkItemActivityContentEnum.WORK_ITEM_RESTORED); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteWorkItem(Long id, Long userId) { + // 1.1 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(id); + // 1.2 校验工作项位于回收站 + if (ObjectUtil.notEqual(PmsWorkItemLifecycleStatusEnum.RECYCLED.getStatus(), workItem.getLifecycleStatus())) { + throw exception(WORK_ITEM_DELETE_STATUS_INVALID); + } + // 1.3 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(workItem.getProjectId(), userId); + // 1.4 校验项目处于进行中 + validateActiveProject(project); + + // 2.1 解除子工作项和关联需求关系 + workItemMapper.updateParentIdToNullByParentId(id); + if (PmsWorkItemTypeEnum.REQUIREMENT.getType().equals(workItem.getType())) { + workItemMapper.updateRelatedRequirementIdToNullByRelatedRequirementId(id); + } + // 2.2 删除参与人、个人排序、评论、工时和动态 + workItemMemberMapper.deleteByWorkItemId(id); + workItemUserSortService.deleteWorkItemUserSortByWorkItemId(id); + workItemCommentService.deleteWorkItemCommentListByWorkItemId(id); + workItemWorkLogService.deleteWorkItemWorkLogListByWorkItemId(id); + workItemActivityService.deleteWorkItemActivityListByWorkItemId(id); + // 2.3 删除工作项主记录 + workItemMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteWorkItemListByProjectId(Long projectId) { + // 1. 删除项目工作项动态、评论、个人排序和参与人 + workItemActivityService.deleteWorkItemActivityListByProjectId(projectId); + List workItems = workItemMapper.selectListByProjectId(projectId); + workItemCommentService.deleteWorkItemCommentListByWorkItemIds(convertSet(workItems, PmsWorkItemDO::getId)); + workItemWorkLogService.deleteWorkItemWorkLogListByProjectId(projectId); + workItemMemberMapper.deleteByProjectId(projectId); + workItemUserSortService.deleteWorkItemUserSortByProjectId(projectId); + + // 2. 删除项目工作项和看板状态 + workItemMapper.deleteByProjectId(projectId); + workItemStatusService.deleteWorkItemStatusListByProjectId(projectId); + } + + @Override + public void clearWorkItemIterationId(Long iterationId) { + workItemMapper.updateIterationIdToNullByIterationId(iterationId); + } + + @Override + public PmsWorkItemDO getWorkItem(Long id, Long userId) { + // 1. 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(id); + + // 2. 校验当前用户可以访问项目 + projectMemberService.validateProjectReadable(workItem.getProjectId(), userId); + return workItem; + } + + @Override + public PmsWorkItemDO getWritableWorkItem(Long id, Long userId) { + // 1.1 校验工作项存在 + PmsWorkItemDO workItem = validateWorkItemExists(id); + // 1.2 校验工作项处于正常状态 + validateActiveWorkItem(workItem); + + // 2.1 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(workItem.getProjectId(), userId); + // 2.2 校验项目处于进行中 + validateActiveProject(project); + return workItem; + } + + @Override + public PageResult getWorkItemPage(PmsWorkItemPageReqVO pageReqVO, Long userId) { + // 1.1 校验当前用户可以访问项目 + PmsProjectDO project = projectMemberService.validateProjectReadable(pageReqVO.getProjectId(), userId); + // 1.2 校验工作项类型可用 + if (pageReqVO.getType() != null) { + validateWorkItemType(project, pageReqVO.getType()); + } + if (CollUtil.isNotEmpty(pageReqVO.getTypes())) { + pageReqVO.getTypes().forEach(type -> validateWorkItemType(project, type)); + } + + // 2. 待规划页面按当前用户的个人顺序分页; + // 【特殊】个人排序存在"无排序记录时保持默认顺序"的短路语义,难以在 SQL 中等价表达,待规划区数据量为项目内未规划根工作项,因此内存排序后分页 + if (Boolean.TRUE.equals(pageReqVO.getPlanningOnly()) && Boolean.TRUE.equals(pageReqVO.getUnplannedOnly())) { + List workItems = workItemMapper.selectListByPlanning(pageReqVO); + workItemUserSortService.sortWorkItemList(workItems, pageReqVO.getProjectId(), userId); + int fromIndex = Math.min((pageReqVO.getPageNo() - 1) * pageReqVO.getPageSize(), workItems.size()); + int toIndex = Math.min(fromIndex + pageReqVO.getPageSize(), workItems.size()); + return new PageResult<>(new ArrayList<>(workItems.subList(fromIndex, toIndex)), (long) workItems.size()); + } + + // 3. 查询其他工作项分页 + return workItemMapper.selectPage(pageReqVO); + } + + @Override + public Map> getProjectWorkItemStatusCountMap(Collection projectIds) { + if (CollUtil.isEmpty(projectIds)) { + return Collections.emptyMap(); + } + return workItemMapper.selectStatusCountMapByProjectIds(projectIds); + } + + @Override + public List getWorkItemList(PmsWorkItemPageReqVO pageReqVO, Long userId) { + // 1. 校验当前用户可以访问项目 + projectMemberService.validateProjectReadable(pageReqVO.getProjectId(), userId); + + // 2. 查询全部符合条件的工作项,工作项类型由查询条件直接过滤 + return workItemMapper.selectList(pageReqVO); + } + + @Override + public List getActiveWorkItemListByProjectId(Long projectId) { + return workItemMapper.selectActiveListByProjectId(projectId); + } + + @Override + public List getActiveWorkItemListByIterationId(Long iterationId) { + return workItemMapper.selectListByIterationId(iterationId); + } + + @Override + public Map> getIterationWorkItemStatusCountMap(Collection iterationIds) { + if (CollUtil.isEmpty(iterationIds)) { + return Collections.emptyMap(); + } + return workItemMapper.selectStatusCountMapByIterationIds(iterationIds); + } + + @Override + public PageResult getAssignedWorkItemPage(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long userId) { + if (CollUtil.isEmpty(projectIds)) { + return PageResult.empty(); + } + return workItemMapper.selectWorkbenchPage(pageReqVO, projectIds, userId, pageReqVO.getProjectId(), + pageReqVO.getType(), pageReqVO.getName(), pageReqVO.getStatus(), pageReqVO.getPriority(), + pageReqVO.getIterationId(), pageReqVO.getEndTime(), + PmsWorkItemStatusTypeEnum.COMPLETED.getType()); + } + + @Override + public Map getAssignedWorkItemTypeCountMap(PmsWorkbenchPageReqVO pageReqVO, + Collection projectIds, Long userId) { + if (CollUtil.isEmpty(projectIds)) { + return Collections.emptyMap(); + } + return workItemMapper.selectWorkbenchTypeCountMap(pageReqVO, projectIds, userId, + PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus(), + PmsWorkItemStatusTypeEnum.COMPLETED.getType()); + } + + @Override + @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 + public PmsWorkItemImportRespVO importWorkItemList(Long projectId, Integer workItemType, + List importRows, Long userId) { + // 1. 参数校验 + PmsProjectDO project = projectMemberService.validateProjectWritable(projectId, userId); + validateActiveProject(project); + validateWorkItemType(project, workItemType); + + // 2. 查询状态和标签 + Map statusIdMap = convertMap(workItemStatusService.getWorkItemStatusList(projectId, workItemType), + PmsWorkItemStatusDO::getName, PmsWorkItemStatusDO::getId); + Map labelIdMap = convertMap(workItemLabelService.getWorkItemLabelList((String) null), + PmsWorkItemLabelDO::getName, PmsWorkItemLabelDO::getId); + + // 3. 遍历,逐个导入 + int successCount = 0; + Map failureReasons = new LinkedHashMap<>(); + for (int index = 0; index < importRows.size(); index++) { + PmsWorkItemImportExcelVO importItem = importRows.get(index); + int rowNumber = index + 2; + try { + // 3.1 转换并校验工作项数据 + PmsWorkItemSaveReqVO saveReqVO = parseImportData(importItem, projectId, workItemType, + statusIdMap, labelIdMap); + Long workItemId = createWorkItem(saveReqVO, userId); + // 3.2 更新指定状态 + if (importItem.getStatusId() != null) { + updateWorkItemStatus(new PmsWorkItemStatusUpdateReqVO() + .setId(workItemId).setStatusId(importItem.getStatusId()), userId); + } + // 3.3 记录工时 + if (importItem.getActualHours() != null) { + workItemWorkLogService.createWorkItemWorkLog(new PmsWorkItemWorkLogSaveReqVO() + .setWorkItemId(workItemId).setActualHours(importItem.getActualHours()) + .setRemainingHours(importItem.getRemainingHours() == null + ? 0 : importItem.getRemainingHours()), userId); + } + successCount++; + } catch (Exception ex) { + failureReasons.put(rowNumber, StrUtil.blankToDefault(ex.getMessage(), "数据校验失败")); + } + } + return PmsWorkItemImportRespVO.builder().successCount(successCount).failureReasons(failureReasons).build(); + } + + /** + * 将 Excel 导入行转换为工作项保存数据 + */ + private PmsWorkItemSaveReqVO parseImportData(PmsWorkItemImportExcelVO importItem, Long projectId, Integer workItemType, + Map statusIdMap, Map labelIdMap) { + // 1.1 解析导入字段 + importItem.setPriority(importItem.getPriority() == null ? PmsWorkItemPriorityEnum.NONE.getPriority() : importItem.getPriority()) + .setProgress(importItem.getProgress() == null ? 0 : importItem.getProgress()) + .setEstimatedHours(importItem.getEstimatedHours() == null ? 0 : importItem.getEstimatedHours()) + .setWorkItemType(workItemType); + // 1.2 解析状态 + if (StrUtil.isNotBlank(importItem.getStatusName())) { + Long statusId = statusIdMap.get(importItem.getStatusName()); + Assert.notNull(statusId, "状态不存在或不属于当前工作项类型:" + importItem.getStatusName()); + importItem.setStatusId(statusId); + } + // 1.3 解析标签 + importItem.setLabelIds(new ArrayList<>(convertSet(importItem.getLabels(), labelName -> { + Long labelId = labelIdMap.get(labelName); + Assert.notNull(labelId, "标签不存在:" + labelName); + return labelId; + }))); + // 1.4 校验导入数据 + ValidationUtils.validate(importItem); + + // 2. 构造并校验工作项 + PmsWorkItemSaveReqVO saveReqVO = BeanUtils.toBean(importItem, PmsWorkItemSaveReqVO.class) + .setProjectId(projectId).setType(workItemType); + ValidationUtils.validate(saveReqVO); + return saveReqVO; + } + + @Override + public List getWorkItemBoard(PmsWorkItemBoardReqVO queryReqVO, Long userId) { + // 1.1 校验当前用户可以访问项目 + projectMemberService.validateProjectReadable(queryReqVO.getProjectId(), userId); + // 1.2 查询看板状态和符合条件的工作项 + List statuses = workItemStatusService + .getWorkItemStatusList(queryReqVO.getProjectId(), queryReqVO.getType()); + List boards = workItemStatusService + .getWorkItemBoardList(queryReqVO.getProjectId(), queryReqVO.getType()); + Map> workItemListMap = new LinkedHashMap<>(); + for (PmsWorkItemDO workItem : workItemMapper.selectRootList(queryReqVO)) { + workItemListMap.computeIfAbsent(workItem.getStatusId(), key -> new ArrayList<>()).add(workItem); + } + + // 3. 按独立看板列顺序组装状态和工作项,未放入看板的状态不展示 + Map> statusListMap = new LinkedHashMap<>(); + for (PmsWorkItemStatusDO status : statuses) { + // 空看板状态不属于任何列,不参与看板展示 + if (StrUtil.isBlank(status.getBoardName())) { + continue; + } + statusListMap.computeIfAbsent(status.getBoardName(), key -> new ArrayList<>()).add(status); + } + List result = new ArrayList<>(); + for (PmsWorkItemBoardDO board : boards) { + PmsWorkItemBoardRespVO column = new PmsWorkItemBoardRespVO().setId(board.getId()) + .setName(board.getName()).setStatuses(new ArrayList<>()).setItems(new ArrayList<>()); + for (PmsWorkItemStatusDO status : statusListMap.getOrDefault(board.getName(), Collections.emptyList())) { + column.getStatuses().add(BeanUtils.toBean(status, PmsWorkItemStatusRespVO.class)); + column.getItems().addAll(BeanUtils.toBean(workItemListMap.getOrDefault( + status.getId(), Collections.emptyList()), PmsWorkItemRespVO.class)); + } + result.add(column); + } + return result; + } + + @Override + public Map> getWorkItemMemberUserIdListMap(Collection workItemIds) { + if (CollUtil.isEmpty(workItemIds)) { + return Collections.emptyMap(); + } + List members = workItemMemberMapper.selectListByWorkItemIds(workItemIds); + return convertMultiMap(members, PmsWorkItemMemberDO::getWorkItemId, PmsWorkItemMemberDO::getUserId); + } + + @Override + public List getWorkItemList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return workItemMapper.selectByIds(ids); + } + + /** + * 校验并获得工作项 + * + * @param id 工作项编号 + * @return 工作项 + */ + private PmsWorkItemDO validateWorkItemExists(Long id) { + PmsWorkItemDO workItem = workItemMapper.selectById(id); + if (workItem == null) { + throw exception(WORK_ITEM_NOT_EXISTS); + } + return workItem; + } + + /** + * 校验工作项处于正常状态 + * + * @param workItem 工作项 + */ + private void validateActiveWorkItem(PmsWorkItemDO workItem) { + if (ObjectUtil.notEqual(PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus(), workItem.getLifecycleStatus())) { + throw exception(WORK_ITEM_LIFECYCLE_STATUS_INVALID); + } + } + + /** + * 校验项目处于进行中 + * + * @param project 项目 + */ + private void validateActiveProject(PmsProjectDO project) { + if (ObjectUtil.notEqual(PmsProjectStatusEnum.ACTIVE.getStatus(), project.getStatus())) { + throw exception(PROJECT_STATUS_INVALID); + } + } + + /** + * 校验项目支持工作项类型 + * + * @param project 项目 + * @param workItemType 工作项类型 + */ + private void validateWorkItemType(PmsProjectDO project, Integer workItemType) { + boolean valid = PmsProjectTypeEnum.GENERAL.getType().equals(project.getType()) + ? PmsWorkItemTypeEnum.TASK.getType().equals(workItemType) + : PmsWorkItemTypeEnum.valueOf(workItemType) != null; + if (!valid) { + throw exception(WORK_ITEM_TYPE_INVALID); + } + } + + /** + * 校验工作项的迭代、父工作项、关联需求和缺陷类型 + * + * @param id 当前工作项编号 + * @param project 项目 + * @param workItemType 工作项类型 + * @param reqVO 工作项信息 + * @param userId 用户编号 + */ + private void validateWorkItemRelations(Long id, PmsProjectDO project, Integer workItemType, + PmsWorkItemSaveReqVO reqVO, Long userId) { + if (reqVO.getIterationId() != null) { + PmsIterationDO iteration = iterationService.getIteration(reqVO.getIterationId(), userId); + if (ObjectUtil.notEqual(project.getId(), iteration.getProjectId()) + || ObjectUtil.notEqual(PmsProjectTypeEnum.AGILE.getType(), project.getType())) { + throw exception(WORK_ITEM_ITERATION_INVALID); + } + } + validateParentWorkItem(id, project.getId(), workItemType, reqVO.getParentId()); + validateRelatedRequirement(project.getId(), workItemType, reqVO.getRelatedRequirementId()); + boolean defectWorkItem = PmsWorkItemTypeEnum.DEFECT.getType().equals(workItemType); + if (defectWorkItem && reqVO.getDefectType() == null + || !defectWorkItem && reqVO.getDefectType() != null) { + throw exception(WORK_ITEM_DEFECT_TYPE_INVALID); + } + } + + /** + * 校验父工作项属于同一项目和类型,且不会形成循环 + * + * @param id 当前工作项编号 + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @param parentId 父工作项编号 + */ + private void validateParentWorkItem(Long id, Long projectId, Integer workItemType, Long parentId) { + Long currentParentId = parentId; + Set visitedIds = new HashSet<>(); + for (int index = 0; index < Short.MAX_VALUE && currentParentId != null; index++) { + if (ObjectUtil.equal(currentParentId, id) || !visitedIds.add(currentParentId)) { + throw exception(WORK_ITEM_PARENT_INVALID); + } + PmsWorkItemDO parent = workItemMapper.selectById(currentParentId); + if (parent == null || ObjectUtil.notEqual(projectId, parent.getProjectId()) + || ObjectUtil.notEqual(workItemType, parent.getType())) { + throw exception(WORK_ITEM_PARENT_INVALID); + } + // 工作项详情只展示单级子项,父工作项不能再挂在其他工作项下 + if (parent.getParentId() != null) { + throw exception(WORK_ITEM_PARENT_INVALID); + } + validateActiveWorkItem(parent); + currentParentId = parent.getParentId(); + } + if (currentParentId != null) { + throw exception(WORK_ITEM_PARENT_INVALID); + } + } + + /** + * 校验关联需求属于同一项目,且当前工作项不是需求 + * + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @param requirementId 需求编号 + */ + private void validateRelatedRequirement(Long projectId, Integer workItemType, Long requirementId) { + if (requirementId == null) { + return; + } + PmsWorkItemDO requirement = workItemMapper.selectById(requirementId); + if (PmsWorkItemTypeEnum.REQUIREMENT.getType().equals(workItemType) || requirement == null + || ObjectUtil.notEqual(projectId, requirement.getProjectId()) + || ObjectUtil.notEqual(PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus(), requirement.getLifecycleStatus()) + || ObjectUtil.notEqual(PmsWorkItemTypeEnum.REQUIREMENT.getType(), requirement.getType())) { + throw exception(WORK_ITEM_RELATED_REQUIREMENT_INVALID); + } + } + + /** + * 校验负责人和参与人都是项目成员 + * + * @param projectId 项目编号 + * @param reqVO 工作项信息 + * @param userId 操作人用户编号 + */ + private void validateWorkItemMembers(Long projectId, PmsWorkItemSaveReqVO reqVO, Long userId) { + Set userIds = new LinkedHashSet<>(); + if (reqVO.getAssigneeUserId() != null) { + userIds.add(reqVO.getAssigneeUserId()); + } + userIds.addAll(getMemberUserIds(reqVO, userId)); + projectMemberService.validateProjectMemberList(projectId, userIds); + } + + /** + * 获得工作项参与人,未选择参与人时默认使用操作人 + * + * @param reqVO 工作项信息 + * @param userId 操作人用户编号 + * @return 参与人用户编号集合 + */ + private Set getMemberUserIds(PmsWorkItemSaveReqVO reqVO, Long userId) { + return CollUtil.isEmpty(reqVO.getMemberUserIds()) ? Collections.singleton(userId) + : new LinkedHashSet<>(reqVO.getMemberUserIds()); + } + + /** + * 保存工作项参与人关系 + * + * @param workItemId 工作项编号 + * @param projectId 项目编号 + * @param memberUserIds 参与人用户编号列表 + */ + private void saveWorkItemMembers(Long workItemId, Long projectId, Collection memberUserIds) { + if (CollUtil.isEmpty(memberUserIds)) { + return; + } + workItemMemberMapper.insertBatch(convertList(memberUserIds, + memberUserId -> new PmsWorkItemMemberDO().setProjectId(projectId) + .setWorkItemId(workItemId).setUserId(memberUserId))); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusService.java new file mode 100644 index 000000000..41288b135 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusService.java @@ -0,0 +1,158 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemBoardConfigSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusConfigUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusDeleteReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * PMS 工作项看板状态 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkItemStatusService { + + /** + * 创建自定义看板状态,追加到项目工作项类型的状态末尾 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 状态编号 + */ + Long createWorkItemStatus(PmsWorkItemStatusCreateReqVO createReqVO, Long userId); + + /** + * 更新看板状态配置,并同步其全部工作项的语义状态 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateWorkItemStatusConfig(PmsWorkItemStatusConfigUpdateReqVO updateReqVO, Long userId); + + /** + * 切换当前工作项类型的初始状态 + * + * @param id 状态编号 + * @param userId 用户编号 + */ + void updateDefaultWorkItemStatus(Long id, Long userId); + + /** + * 更新项目工作项类型的状态顺序,要求排序列表完整覆盖全部状态 + * + * @param sortReqVO 排序信息 + * @param userId 用户编号 + */ + void updateWorkItemStatusSort(PmsWorkItemStatusSortReqVO sortReqVO, Long userId); + + /** + * 保存工作项看板列及其状态映射 + * + * @param saveReqVO 看板配置 + * @param userId 用户编号 + */ + void updateWorkItemBoardConfig(PmsWorkItemBoardConfigSaveReqVO saveReqVO, Long userId); + + /** + * 删除看板状态,存在工作项时先迁移到目标状态,初始状态不允许删除 + * + * @param deleteReqVO 删除信息 + * @param userId 用户编号 + */ + void deleteWorkItemStatus(PmsWorkItemStatusDeleteReqVO deleteReqVO, Long userId); + + /** + * 初始化项目支持的工作项状态 + * + * @param projectId 项目编号 + * @param projectType 项目类型 + */ + void initProjectWorkItemStatuses(Long projectId, Integer projectType); + + /** + * 获得工作项状态,不存在时抛出异常 + * + * @param id 状态编号 + * @return 工作项状态 + */ + PmsWorkItemStatusDO getWorkItemStatus(Long id); + + /** + * 获得用户可读的工作项状态 + * + * @param id 状态编号 + * @param userId 用户编号 + * @return 工作项状态 + */ + PmsWorkItemStatusDO getWorkItemStatus(Long id, Long userId); + + /** + * 获得项目指定工作项类型的已保存状态列表 + * + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @return 已存在的状态列表 + */ + List getWorkItemStatusList(Long projectId, Integer workItemType); + + /** + * 获得项目指定工作项类型的已保存看板列 + * + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @return 看板列列表 + */ + List getWorkItemBoardList(Long projectId, Integer workItemType); + + /** + * 获得工作项的初始状态 + * + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @return 初始状态 + */ + PmsWorkItemStatusDO getDefaultWorkItemStatus(Long projectId, Integer workItemType); + + /** + * 获得项目指定工作项类型的看板状态列表 + * + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @param userId 用户编号 + * @return 状态列表 + */ + List getWorkItemStatusList(Long projectId, Integer workItemType, Long userId); + + /** + * 校验并获得看板状态 + * + * @param id 状态编号 + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @return 看板状态 + */ + PmsWorkItemStatusDO validateWorkItemStatus(Long id, Long projectId, Integer workItemType); + + /** + * 获得工作项状态 Map + * + * @param ids 状态编号集合 + * @return 工作项状态 Map + */ + Map getWorkItemStatusMap(Collection ids); + + /** + * 删除项目的全部工作项状态 + * + * @param projectId 项目编号 + */ + void deleteWorkItemStatusListByProjectId(Long projectId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusServiceImpl.java new file mode 100644 index 000000000..fa24a748b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusServiceImpl.java @@ -0,0 +1,462 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemBoardConfigSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusConfigUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusDeleteReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemStatusMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_DEFAULT_CANNOT_DELETE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_SORT_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_TRANSFER_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_TYPE_INVALID; + +/** + * PMS 工作项看板状态 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsWorkItemStatusServiceImpl implements PmsWorkItemStatusService { + + @Resource + private PmsWorkItemStatusMapper workItemStatusMapper; + + @Resource + private PmsWorkItemBoardService workItemBoardService; + @Lazy // 延迟加载,避免循环依赖 + @Resource + private PmsWorkItemService workItemService; + @Resource + private PmsProjectMemberService projectMemberService; + + @Override + public Long createWorkItemStatus(PmsWorkItemStatusCreateReqVO createReqVO, Long userId) { + // 1.1 校验项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(createReqVO.getProjectId(), userId); + // 1.2 校验项目处于进行中 + validateActiveProject(project); + // 1.3 校验工作项类型可用 + validateWorkItemType(project, createReqVO.getWorkItemType()); + List statuses = getWorkItemStatusList(project.getId(), createReqVO.getWorkItemType()); + if (CollUtil.isEmpty(statuses)) { + throw exception(WORK_ITEM_STATUS_NOT_EXISTS); + } + String name = createReqVO.getName(); + // 1.4 校验状态名称唯一 + validateStatusNameUnique(null, project.getId(), createReqVO.getWorkItemType(), name); + + // 2. 追加自定义状态 + PmsWorkItemStatusDO status = BeanUtils.toBean(createReqVO, PmsWorkItemStatusDO.class) + .setProjectId(project.getId()).setName(name) + .setBoardName(null) + .setDefaultStatus(false).setSort(CollUtil.getLast(statuses).getSort() + 1); + workItemStatusMapper.insert(status); + return status.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemStatusConfig(PmsWorkItemStatusConfigUpdateReqVO updateReqVO, Long userId) { + // 1.1 校验状态存在 + PmsWorkItemStatusDO status = getWorkItemStatus(updateReqVO.getId()); + // 1.2 校验状态所属项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(status.getProjectId(), userId); + // 1.3 校验项目处于进行中 + validateActiveProject(project); + String name = updateReqVO.getName(); + // 1.4 校验状态名称唯一 + validateStatusNameUnique(status.getId(), status.getProjectId(), status.getWorkItemType(), name); + + // 2. 更新状态及其全部工作项的语义状态 + workItemStatusMapper.updateById(BeanUtils.toBean(updateReqVO, PmsWorkItemStatusDO.class) + .setId(status.getId()).setName(name).setBoardName(status.getBoardName())); + workItemService.updateWorkItemStatusTypeByStatusId(status.getId(), updateReqVO.getStatusType()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDefaultWorkItemStatus(Long id, Long userId) { + // 1.1 校验状态存在 + PmsWorkItemStatusDO status = getWorkItemStatus(id); + // 1.2 校验状态所属项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(status.getProjectId(), userId); + // 1.3 校验项目处于进行中 + validateActiveProject(project); + + // 2. 切换当前工作项类型的初始状态 + workItemStatusMapper.updateDefaultStatusByProjectIdAndWorkItemType( + status.getProjectId(), status.getWorkItemType(), id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemStatusSort(PmsWorkItemStatusSortReqVO sortReqVO, Long userId) { + // 1.1 校验排序列表中的状态无重复且全部存在 + List statusIds = sortReqVO.getStatusIds(); + List statuses = workItemStatusMapper.selectByIds(statusIds); + if (statuses.size() != statusIds.size()) { + throw exception(WORK_ITEM_STATUS_SORT_INVALID); + } + PmsWorkItemStatusDO firstStatus = CollUtil.getFirst(statuses); + // 1.2 校验状态所属项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(firstStatus.getProjectId(), userId); + // 1.3 校验项目处于进行中 + validateActiveProject(project); + // 1.4 校验排序列表完整且属于同一项目和工作项类型 + List allStatuses = workItemStatusMapper.selectListByProjectIdAndWorkItemType( + firstStatus.getProjectId(), firstStatus.getWorkItemType()); + if (ObjectUtil.notEqual(convertSet(statusIds), convertSet(allStatuses, PmsWorkItemStatusDO::getId))) { + throw exception(WORK_ITEM_STATUS_SORT_INVALID); + } + + // 2. 按状态编号稳定加锁并写入请求顺序 + Map sortMap = new LinkedHashMap<>(); + for (int index = 0; index < sortReqVO.getStatusIds().size(); index++) { + sortMap.put(sortReqVO.getStatusIds().get(index), index + 1); + } + List sortedStatusIds = new ArrayList<>(statusIds); + Collections.sort(sortedStatusIds); + workItemStatusMapper.updateBatch(convertList(sortedStatusIds, + statusId -> new PmsWorkItemStatusDO().setId(statusId).setSort(sortMap.get(statusId)))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemBoardConfig(PmsWorkItemBoardConfigSaveReqVO saveReqVO, Long userId) { + // 1.1 校验项目和工作项类型可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(saveReqVO.getProjectId(), userId); + validateActiveProject(project); + validateWorkItemType(project, saveReqVO.getWorkItemType()); + // 1.2 校验看板列名称和状态映射不重复,且状态全部属于当前项目和工作项类型 + Set boardNames = new LinkedHashSet<>(); + Set statusIds = new LinkedHashSet<>(); + for (PmsWorkItemBoardConfigSaveReqVO.Board board : saveReqVO.getBoards()) { + if (!boardNames.add(board.getName())) { + throw exception(WORK_ITEM_STATUS_SORT_INVALID); + } + for (Long statusId : board.getStatusIds()) { + if (!statusIds.add(statusId)) { + throw exception(WORK_ITEM_STATUS_SORT_INVALID); + } + } + } + Map statusMap = CollUtil.isEmpty(statusIds) ? Collections.emptyMap() + : convertMap(workItemStatusMapper.selectByIds(statusIds), PmsWorkItemStatusDO::getId); + if (statusMap.size() != statusIds.size() || statusMap.values().stream().anyMatch(status -> + ObjectUtil.notEqual(status.getProjectId(), project.getId()) + || ObjectUtil.notEqual(status.getWorkItemType(), saveReqVO.getWorkItemType()))) { + throw exception(WORK_ITEM_STATUS_SORT_INVALID); + } + + // 2.1 读取原看板列,并校验请求中的列编号属于当前项目和工作项类型 + List oldBoards = workItemBoardService + .getWorkItemBoardList(project.getId(), saveReqVO.getWorkItemType()); + Set oldBoardIds = convertSet(oldBoards, PmsWorkItemBoardDO::getId); + Set newBoardIds = convertSet(saveReqVO.getBoards(), PmsWorkItemBoardConfigSaveReqVO.Board::getId); + if (newBoardIds.size() != saveReqVO.getBoards().stream().filter(board -> board.getId() != null).count() + || !oldBoardIds.containsAll(newBoardIds)) { + throw exception(WORK_ITEM_STATUS_SORT_INVALID); + } + + // 2.2 对比看板列,保留已有编号,仅增删改实际发生变化的列 + List newBoards = convertList(saveReqVO.getBoards(), board -> + new PmsWorkItemBoardDO().setId(board.getId()).setProjectId(project.getId()) + .setWorkItemType(saveReqVO.getWorkItemType()).setName(board.getName()) + .setSort(saveReqVO.getBoards().indexOf(board) + 1)); + List> boardDiff = diffList(oldBoards, newBoards, + (oldBoard, newBoard) -> oldBoard.getId() != null && oldBoard.getId().equals(newBoard.getId())); + if (CollUtil.isNotEmpty(boardDiff.get(0))) { + workItemBoardService.createWorkItemBoardList(boardDiff.get(0)); + } + if (CollUtil.isNotEmpty(boardDiff.get(1))) { + workItemBoardService.updateWorkItemBoardList(boardDiff.get(1)); + } + if (CollUtil.isNotEmpty(boardDiff.get(2))) { + workItemBoardService.deleteWorkItemBoardList(convertList(boardDiff.get(2), PmsWorkItemBoardDO::getId)); + } + + // 2.3 先清除当前类型的旧状态映射,再按请求批量写入新映射 + workItemStatusMapper.updateBoardNameByProjectIdAndWorkItemType( + project.getId(), saveReqVO.getWorkItemType(), null); + for (PmsWorkItemBoardConfigSaveReqVO.Board board : saveReqVO.getBoards()) { + if (CollUtil.isNotEmpty(board.getStatusIds())) { + workItemStatusMapper.updateBoardNameByIds(board.getStatusIds(), board.getName()); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteWorkItemStatus(PmsWorkItemStatusDeleteReqVO deleteReqVO, Long userId) { + // 1.1 校验待删除状态存在 + PmsWorkItemStatusDO sourceStatus = getWorkItemStatus(deleteReqVO.getId()); + // 1.2 校验状态所属项目可编辑 + PmsProjectDO project = projectMemberService.validateProjectWritable(sourceStatus.getProjectId(), userId); + // 1.3 校验项目处于进行中 + validateActiveProject(project); + // 1.4 校验默认状态不能删除 + if (sourceStatus.getDefaultStatus()) { + throw exception(WORK_ITEM_STATUS_DEFAULT_CANNOT_DELETE); + } + + // 2. 存在工作项时迁移到同项目同类型的目标状态 + Long workItemCount = workItemService.getWorkItemCountByStatusId(sourceStatus.getId()); + if (workItemCount > 0 || deleteReqVO.getTransferStatusId() != null) { + if (deleteReqVO.getTransferStatusId() == null + || ObjectUtil.equal(sourceStatus.getId(), deleteReqVO.getTransferStatusId())) { + throw exception(WORK_ITEM_STATUS_TRANSFER_INVALID); + } + PmsWorkItemStatusDO targetStatus = validateWorkItemStatus( + deleteReqVO.getTransferStatusId(), sourceStatus.getProjectId(), sourceStatus.getWorkItemType()); + workItemService.transferWorkItemStatus( + sourceStatus.getId(), targetStatus.getId(), targetStatus.getStatusType()); + } + + // 3. 删除空状态 + workItemStatusMapper.deleteById(sourceStatus.getId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void initProjectWorkItemStatuses(Long projectId, Integer projectType) { + // 1. 通用项目初始化任务状态和看板列 + Integer taskType = PmsWorkItemTypeEnum.TASK.getType(); + initWorkItemStatusList(projectId, taskType); + initWorkItemBoardList(projectId, taskType); + if (PmsProjectTypeEnum.GENERAL.getType().equals(projectType)) { + return; + } + + // 2. 敏捷开发项目同时初始化需求、缺陷状态和对应看板列 + Integer requirementType = PmsWorkItemTypeEnum.REQUIREMENT.getType(); + initWorkItemStatusList(projectId, requirementType); + initWorkItemBoardList(projectId, requirementType); + Integer defectType = PmsWorkItemTypeEnum.DEFECT.getType(); + initWorkItemStatusList(projectId, defectType); + initWorkItemBoardList(projectId, defectType); + } + + @Override + public PmsWorkItemStatusDO getWorkItemStatus(Long id) { + PmsWorkItemStatusDO status = workItemStatusMapper.selectById(id); + if (status == null) { + throw exception(WORK_ITEM_STATUS_NOT_EXISTS); + } + return status; + } + + @Override + public PmsWorkItemStatusDO getWorkItemStatus(Long id, Long userId) { + // 1. 查询工作项状态 + PmsWorkItemStatusDO status = getWorkItemStatus(id); + // 2. 校验项目可读 + projectMemberService.validateProjectReadable(status.getProjectId(), userId); + return status; + } + + private List initWorkItemStatusList(Long projectId, Integer workItemType) { + // 1. 查询项目工作项状态和已有系统状态编码 + List statuses = workItemStatusMapper + .selectListByProjectIdAndWorkItemType(projectId, workItemType); + List defaultStatuses = Arrays.asList(PmsWorkItemStatusTypeEnum.PENDING, + PmsWorkItemStatusTypeEnum.PROCESSING, PmsWorkItemStatusTypeEnum.COMPLETED); + Set existingSystemCodes = convertSet(statuses, PmsWorkItemStatusDO::getSystemCode); + if (existingSystemCodes.containsAll(convertSet(defaultStatuses, Enum::name))) { + return statuses; + } + + // 2. 补充缺失的系统状态,并重新查询 + initMissingSystemWorkItemStatuses(projectId, workItemType, statuses, defaultStatuses, existingSystemCodes); + return workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, workItemType); + } + + private void initWorkItemBoardList(Long projectId, Integer workItemType) { + // 1. 项目创建时已完成初始化,重复初始化直接结束 + List boards = workItemBoardService.getWorkItemBoardList(projectId, workItemType); + if (CollUtil.isNotEmpty(boards)) { + return; + } + + // 2. 按初始化后的状态所属列创建独立看板列 + Set boardNames = new LinkedHashSet<>(); + for (PmsWorkItemStatusDO status : getWorkItemStatusList(projectId, workItemType)) { + if (StrUtil.isBlank(status.getBoardName()) + || !boardNames.add(status.getBoardName())) { + continue; + } + workItemBoardService.createWorkItemBoard(new PmsWorkItemBoardDO().setProjectId(projectId) + .setWorkItemType(workItemType).setName(status.getBoardName()).setSort(boardNames.size())); + } + } + + @Override + public List getWorkItemStatusList(Long projectId, Integer workItemType) { + return workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, workItemType); + } + + @Override + public List getWorkItemBoardList(Long projectId, Integer workItemType) { + return workItemBoardService.getWorkItemBoardList(projectId, workItemType); + } + + /** + * 初始化缺失的未开始、进行中和已完成系统状态 + * + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @param statuses 已有状态列表 + * @param defaultStatuses 系统状态类型列表 + * @param existingSystemCodes 已有系统状态编码集合 + */ + private void initMissingSystemWorkItemStatuses(Long projectId, Integer workItemType, + List statuses, + List defaultStatuses, + Set existingSystemCodes) { + boolean hasDefaultStatus = CollUtil.findOne( + statuses, status -> Boolean.TRUE.equals(status.getDefaultStatus())) != null; + for (int index = 0; index < defaultStatuses.size(); index++) { + PmsWorkItemStatusTypeEnum statusType = defaultStatuses.get(index); + if (existingSystemCodes.contains(statusType.name())) { + continue; + } + try { + workItemStatusMapper.insert(new PmsWorkItemStatusDO().setProjectId(projectId) + .setWorkItemType(workItemType).setName(statusType.getName()) + .setStatusType(statusType.getType()).setSystemCode(statusType.name()) + .setBoardName(statusType.getName()) + .setDefaultStatus(!hasDefaultStatus && PmsWorkItemStatusTypeEnum.PENDING.equals(statusType)) + .setSort(index + 1)); + if (PmsWorkItemStatusTypeEnum.PENDING.equals(statusType)) { + hasDefaultStatus = true; + } + } catch (DuplicateKeyException ignored) { + // 其他请求已完成同一默认状态的初始化,继续查询即可 + } + } + } + + @Override + public PmsWorkItemStatusDO getDefaultWorkItemStatus(Long projectId, Integer workItemType) { + PmsWorkItemStatusDO status = workItemStatusMapper + .selectDefaultByProjectIdAndWorkItemType(projectId, workItemType); + if (status == null) { + throw exception(WORK_ITEM_STATUS_NOT_EXISTS); + } + return status; + } + + @Override + public List getWorkItemStatusList(Long projectId, Integer workItemType, Long userId) { + // 1. 校验当前用户可以访问项目 + projectMemberService.validateProjectReadable(projectId, userId); + // 2. 查询已保存的工作项状态列表,不在读取接口初始化数据 + return getWorkItemStatusList(projectId, workItemType); + } + + @Override + public PmsWorkItemStatusDO validateWorkItemStatus(Long id, Long projectId, Integer workItemType) { + PmsWorkItemStatusDO status = getWorkItemStatus(id); + if (ObjectUtil.notEqual(projectId, status.getProjectId()) + || ObjectUtil.notEqual(workItemType, status.getWorkItemType())) { + throw exception(WORK_ITEM_STATUS_INVALID); + } + return status; + } + + @Override + public Map getWorkItemStatusMap(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyMap(); + } + return convertMap(workItemStatusMapper.selectByIds(ids), PmsWorkItemStatusDO::getId); + } + + @Override + public void deleteWorkItemStatusListByProjectId(Long projectId) { + workItemBoardService.deleteWorkItemBoardListByProjectId(projectId); + workItemStatusMapper.deleteByProjectId(projectId); + } + + /** + * 校验项目处于进行中 + * + * @param project 项目 + */ + private void validateActiveProject(PmsProjectDO project) { + if (ObjectUtil.notEqual(PmsProjectStatusEnum.ACTIVE.getStatus(), project.getStatus())) { + throw exception(PROJECT_STATUS_INVALID); + } + } + + /** + * 校验项目支持工作项类型 + * + * @param project 项目 + * @param workItemType 工作项类型 + */ + private void validateWorkItemType(PmsProjectDO project, Integer workItemType) { + boolean valid = PmsProjectTypeEnum.GENERAL.getType().equals(project.getType()) + ? PmsWorkItemTypeEnum.TASK.getType().equals(workItemType) + : PmsWorkItemTypeEnum.valueOf(workItemType) != null; + if (!valid) { + throw exception(WORK_ITEM_TYPE_INVALID); + } + } + + /** + * 校验同一项目和工作项类型下状态名称唯一 + * + * @param id 状态编号 + * @param projectId 项目编号 + * @param workItemType 工作项类型 + * @param name 状态名称 + */ + private void validateStatusNameUnique(Long id, Long projectId, Integer workItemType, String name) { + PmsWorkItemStatusDO status = workItemStatusMapper + .selectByProjectIdAndWorkItemTypeAndName(projectId, workItemType, name); + if (status != null && ObjectUtil.notEqual(status.getId(), id)) { + throw exception(WORK_ITEM_STATUS_NAME_DUPLICATE); + } + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortService.java new file mode 100644 index 000000000..0fa68e9ab --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortService.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; + +import java.util.List; + +/** + * PMS 工作项个人排序 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkItemUserSortService { + + /** + * 更新用户的待规划工作项顺序 + * + * @param projectId 项目编号 + * @param workItemIds 工作项编号列表 + * @param userId 用户编号 + */ + void updateWorkItemUserSort(Long projectId, List workItemIds, Long userId); + + /** + * 按用户保存的顺序排列待规划工作项 + * + * @param workItems 工作项列表 + * @param projectId 项目编号 + * @param userId 用户编号 + */ + void sortWorkItemList(List workItems, Long projectId, Long userId); + + /** + * 删除工作项的个人排序 + * + * @param workItemId 工作项编号 + */ + void deleteWorkItemUserSortByWorkItemId(Long workItemId); + + /** + * 删除项目的个人排序 + * + * @param projectId 项目编号 + */ + void deleteWorkItemUserSortByProjectId(Long projectId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImpl.java new file mode 100644 index 000000000..c40f3db0f --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImpl.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemUserSortMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * PMS 工作项个人排序 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsWorkItemUserSortServiceImpl implements PmsWorkItemUserSortService { + + @Resource + private PmsWorkItemUserSortMapper workItemUserSortMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemUserSort(Long projectId, List workItemIds, Long userId) { + // 1. 删除原有个人排序 + workItemUserSortMapper.deleteByProjectIdAndUserId(projectId, userId); + + // 2. 按请求顺序保存个人排序 + for (int index = 0; index < workItemIds.size(); index++) { + workItemUserSortMapper.insert(new PmsWorkItemUserSortDO().setProjectId(projectId) + .setWorkItemId(workItemIds.get(index)).setUserId(userId).setSort(index + 1)); + } + } + + @Override + public void sortWorkItemList(List workItems, Long projectId, Long userId) { + // 1. 查询用户保存的个人排序 + List userSorts = + workItemUserSortMapper.selectListByProjectIdAndUserId(projectId, userId); + if (CollUtil.isEmpty(userSorts)) { + return; + } + + // 2. 转换个人排序 Map,并排列工作项 + Map userSortMap = convertMap(userSorts, + PmsWorkItemUserSortDO::getWorkItemId, PmsWorkItemUserSortDO::getSort); + workItems.sort(Comparator + .comparing((PmsWorkItemDO item) -> userSortMap.containsKey(item.getId()) ? 0 : 1) + .thenComparing(item -> userSortMap.getOrDefault(item.getId(), 0)) + .thenComparing(PmsWorkItemDO::getId, Comparator.reverseOrder())); + } + + @Override + public void deleteWorkItemUserSortByWorkItemId(Long workItemId) { + workItemUserSortMapper.deleteByWorkItemId(workItemId); + } + + @Override + public void deleteWorkItemUserSortByProjectId(Long projectId) { + workItemUserSortMapper.deleteByProjectId(projectId); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogService.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogService.java new file mode 100644 index 000000000..84a6c04a1 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogService.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsProjectWorkLogReportReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsProjectWorkLogReportRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSummaryRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; + +import java.util.Collection; +import java.util.List; + +/** + * PMS 工作项工时记录 Service 接口 + * + * @author 芋道源码 + */ +public interface PmsWorkItemWorkLogService { + + /** + * 创建工时记录 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + * @return 工时记录编号 + */ + Long createWorkItemWorkLog(PmsWorkItemWorkLogSaveReqVO saveReqVO, Long userId); + + /** + * 更新工时记录 + * + * @param saveReqVO 保存信息 + * @param userId 用户编号 + */ + void updateWorkItemWorkLog(PmsWorkItemWorkLogSaveReqVO saveReqVO, Long userId); + + /** + * 获得工时记录 + * + * @param id 工时记录编号 + * @param userId 用户编号 + * @return 工时记录 + */ + PmsWorkItemWorkLogDO getWorkItemWorkLog(Long id, Long userId); + + /** + * 获得工作项工时汇总 + * + * @param workItemId 工作项编号 + * @param userId 用户编号 + * @return 工时汇总 + */ + PmsWorkItemWorkLogSummaryRespVO getWorkItemWorkLogSummary(Long workItemId, Long userId); + + /** + * 获得多个工作项的工时记录,供迭代燃尽统计使用 + * + * @param workItemIds 工作项编号集合 + * @return 工时记录 + */ + List getWorkItemWorkLogListByWorkItemIds(Collection workItemIds); + + /** + * 获得项目工时报表,按迭代和工作项汇总日期范围内的每日工时 + * + * @param reqVO 报表查询条件 + * @param userId 用户编号 + * @return 工时报表 + */ + PmsProjectWorkLogReportRespVO getProjectWorkItemWorkLogReport(PmsProjectWorkLogReportReqVO reqVO, Long userId); + + /** + * 删除项目的全部工时记录 + * + * @param projectId 项目编号 + */ + void deleteWorkItemWorkLogListByProjectId(Long projectId); + + /** + * 删除工作项的全部工时,用于工作项删除时级联清理 + * + * @param workItemId 工作项编号 + */ + void deleteWorkItemWorkLogListByWorkItemId(Long workItemId); + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogServiceImpl.java b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogServiceImpl.java new file mode 100644 index 000000000..e58d3daf5 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogServiceImpl.java @@ -0,0 +1,239 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsProjectWorkLogReportReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsProjectWorkLogReportRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSummaryRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemWorkLogMapper; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_WORK_LOG_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_WORK_LOG_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.WORK_LOG_CREATED; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.WORK_LOG_UPDATED; + +/** + * PMS 工作项工时记录 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PmsWorkItemWorkLogServiceImpl implements PmsWorkItemWorkLogService { + + @Resource + private PmsWorkItemWorkLogMapper workLogMapper; + + @Resource + private PmsWorkItemService workItemService; + @Resource + private PmsProjectMemberService projectMemberService; + @Resource + private PmsWorkItemActivityService workItemActivityService; + @Resource + private PmsIterationService iterationService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createWorkItemWorkLog(PmsWorkItemWorkLogSaveReqVO saveReqVO, Long userId) { + // 1. 校验工作项可编辑 + PmsWorkItemDO workItem = workItemService.getWritableWorkItem(saveReqVO.getWorkItemId(), userId); + + // 2.1 创建工时记录 + PmsWorkItemWorkLogDO workLog = BeanUtils.toBean(saveReqVO, PmsWorkItemWorkLogDO.class) + .setProjectId(workItem.getProjectId()).setDescription(StrUtil.trim(saveReqVO.getDescription())); + workLogMapper.insert(workLog); + // 2.2 记录工时创建动态 + workItemActivityService.createWorkItemActivity( + workItem.getProjectId(), workItem.getId(), userId, WORK_LOG_CREATED, saveReqVO.getActualHours()); + return workLog.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkItemWorkLog(PmsWorkItemWorkLogSaveReqVO saveReqVO, Long userId) { + // 1.1 校验工时记录存在 + PmsWorkItemWorkLogDO workLog = validateWorkLogExists(saveReqVO.getId()); + // 1.2 校验工时记录属于请求工作项 + if (ObjectUtil.notEqual(workLog.getWorkItemId(), saveReqVO.getWorkItemId())) { + throw exception(WORK_ITEM_WORK_LOG_INVALID); + } + // 1.3 校验工作项可编辑 + PmsWorkItemDO workItem = workItemService.getWritableWorkItem(workLog.getWorkItemId(), userId); + + // 2.1 更新投入工时和说明 + workLogMapper.updateForEdit(BeanUtils.toBean(saveReqVO, PmsWorkItemWorkLogDO.class) + .setId(workLog.getId()).setDescription(StrUtil.trim(saveReqVO.getDescription()))); + // 2.2 记录工时更新动态 + workItemActivityService.createWorkItemActivity( + workItem.getProjectId(), workItem.getId(), userId, WORK_LOG_UPDATED); + } + + @Override + public PmsWorkItemWorkLogDO getWorkItemWorkLog(Long id, Long userId) { + // 1. 校验工时记录存在 + PmsWorkItemWorkLogDO workLog = validateWorkLogExists(id); + // 2. 校验工作项可查看 + workItemService.getWorkItem(workLog.getWorkItemId(), userId); + return workLog; + } + + @Override + public PmsWorkItemWorkLogSummaryRespVO getWorkItemWorkLogSummary(Long workItemId, Long userId) { + // 1.1 校验工作项可查看 + PmsWorkItemDO workItem = workItemService.getWorkItem(workItemId, userId); + // 1.2 查询工作项工时记录 + List workLogs = workLogMapper.selectListByWorkItemId(workItemId); + + // 2. 汇总已登记工时,剩余工时取最新一条登记记录 + int actualHours = workLogs.stream().mapToInt(PmsWorkItemWorkLogDO::getActualHours).sum(); + Integer remainingHours = CollUtil.isNotEmpty(workLogs) + ? CollUtil.getFirst(workLogs).getRemainingHours() : workItem.getEstimatedHours(); + if (remainingHours == null && workItem.getEstimatedHours() != null) { + remainingHours = Math.max(workItem.getEstimatedHours() - actualHours, 0); + } + List workLogVOs = convertList(workLogs, workLog -> + BeanUtils.toBean(workLog, PmsWorkItemWorkLogRespVO.class) + .setCreatorUserId(NumberUtils.parseLong(workLog.getCreator()))); + return new PmsWorkItemWorkLogSummaryRespVO().setEstimatedHours(workItem.getEstimatedHours()) + .setActualHours(actualHours).setRemainingHours(remainingHours).setRecords(workLogVOs); + } + + @Override + public List getWorkItemWorkLogListByWorkItemIds(Collection workItemIds) { + if (CollUtil.isEmpty(workItemIds)) { + return Collections.emptyList(); + } + return workLogMapper.selectListByWorkItemIds(workItemIds); + } + + @Override + public PmsProjectWorkLogReportRespVO getProjectWorkItemWorkLogReport( + PmsProjectWorkLogReportReqVO reqVO, Long userId) { + // 1.1 校验项目可访问 + projectMemberService.validateProjectReadable(reqVO.getProjectId(), userId); + // 1.2 校验工时登记时间范围 + LocalDateTime beginTime = reqVO.getCreateTime()[0]; + LocalDateTime endTime = reqVO.getCreateTime()[1]; + if (endTime.isBefore(beginTime)) { + throw exception(WORK_ITEM_WORK_LOG_INVALID); + } + + // 2.1 查询范围内工时和关联工作项 + List workLogs = workLogMapper.selectListByProjectIdAndCreateTimeBetween( + reqVO.getProjectId(), beginTime, endTime); + Map workItemMap = convertMap(workItemService.getWorkItemList( + convertSet(workLogs, PmsWorkItemWorkLogDO::getWorkItemId)), PmsWorkItemDO::getId); + // 2.2 查询迭代并按迭代名称筛选 + Set iterationIds = convertSet(workItemMap.values(), PmsWorkItemDO::getIterationId); + iterationIds.remove(null); + Map iterationMap = convertMap(iterationService.getIterationList(iterationIds), + PmsIterationDO::getId); + if (StrUtil.isNotBlank(reqVO.getIterationName())) { + workItemMap.entrySet().removeIf(entry -> { + PmsIterationDO iteration = entry.getValue().getIterationId() != null + ? iterationMap.get(entry.getValue().getIterationId()) : null; + String currentIterationName = iteration != null ? iteration.getName() : "未规划事项"; + return !StrUtil.containsIgnoreCase(currentIterationName, reqVO.getIterationName()); + }); + workLogs.removeIf(workLog -> !workItemMap.containsKey(workLog.getWorkItemId())); + } + + // 3. 按迭代和工作项汇总每日工时 + Map> groupedWorkItems = convertMultiMap(workItemMap.values(), + item -> item.getIterationId() != null ? item.getIterationId() : 0L); + Map> groupedWorkLogs = convertMultiMap( + workLogs, PmsWorkItemWorkLogDO::getWorkItemId); + List groups = new ArrayList<>(); + for (Map.Entry> entry : groupedWorkItems.entrySet()) { + Long iterationId = entry.getKey() != 0L ? entry.getKey() : null; + List items = new ArrayList<>(); + for (PmsWorkItemDO workItem : entry.getValue()) { + Map dailyHours = new LinkedHashMap<>(); + for (PmsWorkItemWorkLogDO workLog : groupedWorkLogs.getOrDefault( + workItem.getId(), Collections.emptyList())) { + dailyHours.merge(workLog.getCreateTime().toLocalDate().toString(), + workLog.getActualHours(), Integer::sum); + } + int totalHours = dailyHours.values().stream().mapToInt(Integer::intValue).sum(); + items.add(new PmsProjectWorkLogReportRespVO.Item().setWorkItemId(workItem.getId()) + .setSerialNumber(workItem.getSerialNumber()).setName(workItem.getName()) + .setType(workItem.getType()).setTotalHours(totalHours).setDailyHours(dailyHours)); + } + items.sort(Comparator.comparingInt(PmsProjectWorkLogReportRespVO.Item::getSerialNumber)); + PmsIterationDO iteration = iterationId != null ? iterationMap.get(iterationId) : null; + groups.add(new PmsProjectWorkLogReportRespVO.Group().setIterationId(iterationId) + .setIterationName(iteration != null ? iteration.getName() : "未规划事项") + .setTotalHours(items.stream().mapToInt(PmsProjectWorkLogReportRespVO.Item::getTotalHours).sum()) + .setItems(items)); + } + groups.sort((first, second) -> first.getIterationId() == null ? 1 + : second.getIterationId() == null ? -1 : first.getIterationName().compareTo(second.getIterationName())); + + // 4. 返回连续日期轴和总工时 + LocalDate beginDate = beginTime.toLocalDate(); + LocalDate endDate = endTime.toLocalDate(); + List dates = new ArrayList<>(); + for (LocalDate date = beginDate; !date.isAfter(endDate); date = date.plusDays(1)) { + dates.add(date.toString()); + } + return new PmsProjectWorkLogReportRespVO().setDates(dates) + .setTotalHours(groups.stream().mapToInt(PmsProjectWorkLogReportRespVO.Group::getTotalHours).sum()) + .setGroups(groups); + } + + @Override + public void deleteWorkItemWorkLogListByProjectId(Long projectId) { + workLogMapper.deleteByProjectId(projectId); + } + + @Override + public void deleteWorkItemWorkLogListByWorkItemId(Long workItemId) { + workLogMapper.deleteByWorkItemId(workItemId); + } + + /** + * 校验工时记录存在 + * + * @param id 工时记录编号 + * @return 工时记录 + */ + private PmsWorkItemWorkLogDO validateWorkLogExists(Long id) { + PmsWorkItemWorkLogDO workLog = workLogMapper.selectById(id); + if (workLog == null) { + throw exception(WORK_ITEM_WORK_LOG_NOT_EXISTS); + } + return workLog; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/resources/application-dev.yaml b/yudao-module-pms/yudao-module-pms-server/src/main/resources/application-dev.yaml new file mode 100644 index 000000000..522c0145e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/resources/application-dev.yaml @@ -0,0 +1,120 @@ +--- #################### 注册中心 + 配置中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + username: # Nacos 账号 + password: # Nacos 密码 + discovery: # 【配置中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + config: # 【注册中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) + max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 + max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 + primary: master + datasource: + master: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 400-infra.server.iocoder.cn # 地址 + port: 6379 # 端口 + database: 1 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + username: admin + password: admin + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + access-log: # 访问日志的配置项 + enable: false + demo: false # 关闭演示模式 diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/resources/application-local.yaml b/yudao-module-pms/yudao-module-pms-server/src/main/resources/application-local.yaml new file mode 100644 index 000000000..1f3360add --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/resources/application-local.yaml @@ -0,0 +1,142 @@ +--- #################### 注册中心 + 配置中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + username: # Nacos 账号 + password: # Nacos 密码 + discovery: # 【配置中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + config: # 【注册中心】配置项 + namespace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 1 # 初始连接数 + min-idle: 1 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) + max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 + max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 + primary: master + datasource: + master: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # 瀚高 HighGo 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例 + # url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例 + username: root + password: 123456 + # username: sa # SQL Server 连接的示例 + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例 + # username: SYSDBA # DM 连接的示例 + # password: SYSDBA # DM 连接的示例 + # username: root # 瀚高 HighGo 连接的示例 + # password: 123456 # 瀚高 HighGo 连接的示例 + slave: # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + username: admin + password: admin + +# 日志文件配置 +logging: + level: + # 配置自己写的 MyBatis Mapper 打印日志 + cn.iocoder.yudao.module.pms.dal.mysql: debug + org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + env: # 多环境的配置项 + tag: ${HOSTNAME} + security: + mock-enable: true + access-log: # 访问日志的配置项 + enable: false diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/resources/application.yaml b/yudao-module-pms/yudao-module-pms-server/src/main/resources/application.yaml new file mode 100644 index 000000000..50c12c106 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/resources/application.yaml @@ -0,0 +1,124 @@ +spring: + application: + name: pms-server + + profiles: + active: local + + main: + allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务 + + config: + import: + - optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置 + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 + + # Servlet 配置 + servlet: + # 文件上传相关配置项 + multipart: + max-file-size: 16MB # 单个文件大小 + max-request-size: 32MB # 设置总上传的文件大小 + + # Jackson 配置项 + jackson: + serialization: + write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳 + write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 + fail-on-empty-beans: false # 允许序列化无属性的 Bean + + # Cache 配置项 + cache: + type: REDIS + redis: + time-to-live: 1h # 设置过期时间为 1 小时 + +server: + port: 48096 + +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + +--- #################### 接口文档配置 #################### + +springdoc: + api-docs: + enabled: true # 1. 是否开启 Swagger 接文档的元数据 + path: /v3/api-docs + swagger-ui: + enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面 + path: /swagger-ui + default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 + +knife4j: + enable: true + setting: + language: zh_cn + +# MyBatis Plus 的配置项 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + global-config: + db-config: + id-type: NONE # "智能"模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 + # id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 + # id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 + # id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + banner: false # 关闭控制台的 Banner 打印 + type-aliases-package: ${yudao.info.base-package}.dal.dataobject + encryptor: + password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 + +mybatis-plus-join: + banner: false # 关闭控制台的 Banner 打印 + +# Spring Data Redis 配置 +spring: + data: + redis: + repositories: + enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度 + +# VO 转换(数据翻译)相关 +easy-trans: + is-enable-global: false # 【默认禁用,对性能确认压力大】启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### + +xxl: + job: + executor: + appname: ${spring.application.name} # 执行器 AppName + logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径 + accessToken: default_token # 执行器通讯TOKEN + +--- #################### 芋道相关配置 #################### + +yudao: + info: + version: 1.0.0 + base-package: cn.iocoder.yudao.module.pms + web: + admin-ui: + url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址 + xss: + enable: false + exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + swagger: + title: 管理后台 + description: 提供管理员管理的所有功能 + version: ${yudao.info.version} + tenant: # 多租户相关配置项 + enable: true + ignore-urls: + +debug: false diff --git a/yudao-module-pms/yudao-module-pms-server/src/main/resources/logback-spring.xml b/yudao-module-pms/yudao-module-pms-server/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..15b28cfdf --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/main/resources/logback-spring.xml @@ -0,0 +1,56 @@ + + + + + + + + +       + + ${CONSOLE_LOG_PATTERN} + + + + + + + + ${FILE_LOG_PATTERN} + + + ${LOG_FILE} + + + ${LOG_FILE}.%d{yyyy-MM-dd}.%i.log + 30 + 10MB + + + + + 0 + 512 + + + + + + + + + + + + + + + diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentShareControllerTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentShareControllerTest.java new file mode 100644 index 000000000..30e927988 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/controller/admin/kb/interaction/PmsKnowledgeDocumentShareControllerTest.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.pms.controller.admin.kb.interaction; + +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.share.PmsKnowledgeDocumentSharePublicRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentShareService; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeDocumentShareController} 的单元测试类 + * + * @author 芋道源码 + */ +public class PmsKnowledgeDocumentShareControllerTest { + + @Test + public void testGetDocumentByToken_fileUsesDefaultPreviewUrl() { + // mock 数据 + String token = "share-token"; + String rawUrl = "https://file.example.com/private/demo.pdf"; + String previewUrl = "https://file.example.com/private/demo.pdf?signature=temporary"; + PmsKnowledgeDocumentDO document = new PmsKnowledgeDocumentDO().setId(1L).setTitle("测试文件") + .setType(PmsKnowledgeDocumentTypeEnum.FILE.getType()).setContent(rawUrl).setFileType("pdf"); + PmsKnowledgeDocumentShareService shareService = mock(PmsKnowledgeDocumentShareService.class); + FileApi fileApi = mock(FileApi.class); + when(shareService.getDocumentByShareToken(token)).thenReturn(document); + when(fileApi.presignGetUrl(rawUrl, null)).thenReturn(success(previewUrl)); + PmsKnowledgeDocumentShareController controller = new PmsKnowledgeDocumentShareController(); + ReflectionTestUtils.setField(controller, "documentShareService", shareService); + ReflectionTestUtils.setField(controller, "fileApi", fileApi); + + // 调用 + PmsKnowledgeDocumentSharePublicRespVO result = controller.getDocumentByToken(token).getData(); + + // 断言 + assertNull(result.getContent()); + assertEquals(previewUrl, result.getPreviewUrl()); + verify(fileApi).presignGetUrl(rawUrl, null); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionServiceImplTest.java new file mode 100644 index 000000000..59f10c095 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeContentPermissionServiceImplTest.java @@ -0,0 +1,280 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeContentPermissionMemberDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeContentPermissionMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeContentPermissionMemberMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeFolderMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMemberMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeContentLevelEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeContentPermissionServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeContentPermissionServiceImpl.class) +public class PmsKnowledgeContentPermissionServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeContentPermissionServiceImpl permissionService; + + @Resource + private PmsKnowledgeContentPermissionMapper permissionMapper; + @Resource + private PmsKnowledgeContentPermissionMemberMapper permissionMemberMapper; + @Resource + private PmsKnowledgeFolderMapper folderMapper; + @Resource + private PmsKnowledgeLibraryMemberMapper libraryMemberMapper; + + @MockitoBean + private PmsKnowledgeLibraryMemberService libraryMemberService; + @MockitoBean + private PmsKnowledgeFolderService folderService; + @MockitoBean + private PmsKnowledgeDocumentService documentService; + @MockitoBean + private AdminUserApi adminUserApi; + @MockitoBean + private DeptApi deptApi; + @MockitoBean + private PermissionApi permissionApi; + + @BeforeEach + public void setUpCloudApiMocks() { + lenient().when(permissionApi.hasAnyRoles(anyLong(), any(String[].class))).thenReturn(success(false)); + lenient().when(adminUserApi.getUser(anyLong())).thenReturn(success(null)); + lenient().when(adminUserApi.validateUserList(anyCollection())).thenReturn(success(true)); + lenient().when(deptApi.validateDeptList(anyCollection())).thenReturn(success(true)); + } + + @Test + public void testCreateDefaultPermission() { + // 准备参数 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + + // 调用 + Long permissionId = permissionService.createDefaultContentPermission(libraryId, userId); + + // 断言 + PmsKnowledgeContentPermissionDO permission = permissionMapper.selectById(permissionId); + assertNotNull(permission); + assertEquals(libraryId, permission.getLibraryId()); + assertTrue(permission.getOpenStatus()); + assertEquals(PmsKnowledgeContentLevelEnum.PREVIEW.getLevel(), permission.getOpenLevel()); + PmsKnowledgeContentPermissionMemberDO owner = permissionMemberMapper.selectByPermissionIdAndUserId(permissionId, userId); + assertNotNull(owner); + assertEquals(PmsKnowledgeContentLevelEnum.MANAGE.getLevel(), owner.getLevel()); + } + + @Test + public void testGetCurrentUserLevel_openBeforeUserAndDept() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + Long deptId = randomLongId(); + PmsKnowledgeContentPermissionDO permission = randomPermissionDO(libraryId, true, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(permission); + permissionMemberMapper.insert(new PmsKnowledgeContentPermissionMemberDO().setPermissionId(permission.getId()).setDeptId(deptId) + .setLevel(PmsKnowledgeContentLevelEnum.DOWNLOAD.getLevel())); + permissionMemberMapper.insert(new PmsKnowledgeContentPermissionMemberDO().setPermissionId(permission.getId()).setUserId(userId) + .setLevel(PmsKnowledgeContentLevelEnum.EDIT.getLevel())); + + // mock 方法 + when(libraryMemberService.isLibraryAdmin(libraryId, userId)).thenReturn(false); + when(adminUserApi.getUser(userId)).thenReturn(success( + new AdminUserRespDTO().setId(userId).setDeptId(deptId))); + + // 调用 + Integer level = permissionService.getCurrentUserContentPermissionLevel(permission.getId(), libraryId, userId); + + // 断言 + assertEquals(PmsKnowledgeContentLevelEnum.PREVIEW.getLevel(), level); + } + + @Test + public void testGetContentPermissionMemberList_readableUser() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + Long collaboratorUserId = randomLongId(); + PmsKnowledgeContentPermissionDO permission = randomPermissionDO(libraryId, true, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(permission); + permissionMemberMapper.insert(new PmsKnowledgeContentPermissionMemberDO() + .setPermissionId(permission.getId()).setUserId(collaboratorUserId) + .setLevel(PmsKnowledgeContentLevelEnum.EDIT.getLevel())); + + // mock 方法 + when(libraryMemberService.isLibraryAdmin(libraryId, userId)).thenReturn(false); + when(adminUserApi.getUser(userId)).thenReturn(success(new AdminUserRespDTO().setId(userId))); + + // 调用 + Set memberUserIds = new LinkedHashSet<>(); + permissionService.getContentPermissionMemberList(permission.getId(), userId) + .forEach(member -> memberUserIds.add(member.getUserId())); + + // 断言 + assertEquals(Collections.singleton(collaboratorUserId), memberUserIds); + verify(libraryMemberService).validateLibraryReadable(libraryId, userId); + } + + @Test + public void testGetCurrentUserLevel_privateDenied() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + PmsKnowledgeContentPermissionDO permission = randomPermissionDO(libraryId, false, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(permission); + + // mock 方法 + when(libraryMemberService.isLibraryAdmin(libraryId, userId)).thenReturn(false); + when(adminUserApi.getUser(userId)).thenReturn(success(new AdminUserRespDTO().setId(userId))); + + // 调用 + Integer level = permissionService.getCurrentUserContentPermissionLevel(permission.getId(), libraryId, userId); + + // 断言 + assertNull(level); + } + + @Test + public void testClonePermissions_copiesMembersAndChangesLibrary() { + // mock 数据 + Long sourceLibraryId = randomLongId(); + Long targetLibraryId = randomLongId(); + Long userId = randomLongId(); + PmsKnowledgeContentPermissionDO source = randomPermissionDO(sourceLibraryId, false, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(source); + permissionMemberMapper.insert(new PmsKnowledgeContentPermissionMemberDO().setPermissionId(source.getId()).setUserId(userId) + .setLevel(PmsKnowledgeContentLevelEnum.MANAGE.getLevel())); + + // 调用 + Map clonedPermissionIdMap = permissionService.cloneContentPermissions( + Collections.singleton(source.getId()), targetLibraryId); + + // 断言 + Long targetPermissionId = clonedPermissionIdMap.get(source.getId()); + assertNotNull(targetPermissionId); + assertNotEquals(source.getId(), targetPermissionId); + PmsKnowledgeContentPermissionDO target = permissionMapper.selectById(targetPermissionId); + assertEquals(targetLibraryId, target.getLibraryId()); + assertFalse(target.getOpenStatus()); + assertNotNull(permissionMemberMapper.selectByPermissionIdAndUserId(targetPermissionId, userId)); + } + + @Test + public void testGetReadablePermissionIds_batchAcrossLibraries() { + // mock 数据 + Long userId = randomLongId(); + Long deptId = randomLongId(); + Long adminLibraryId = randomLongId(); + Long collaboratorLibraryId = randomLongId(); + Long deniedLibraryId = randomLongId(); + PmsKnowledgeContentPermissionDO adminPermission = randomPermissionDO(adminLibraryId, false, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(adminPermission); + PmsKnowledgeContentPermissionDO collaboratorPermission = randomPermissionDO(collaboratorLibraryId, false, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(collaboratorPermission); + PmsKnowledgeContentPermissionDO deniedPermission = randomPermissionDO(deniedLibraryId, false, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(deniedPermission); + libraryMemberMapper.insert(new PmsKnowledgeLibraryMemberDO().setLibraryId(adminLibraryId).setUserId(userId) + .setLevel(PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel())); + permissionMemberMapper.insert(new PmsKnowledgeContentPermissionMemberDO() + .setPermissionId(collaboratorPermission.getId()).setDeptId(deptId) + .setLevel(PmsKnowledgeContentLevelEnum.PREVIEW.getLevel())); + + // mock 方法 + when(adminUserApi.getUser(userId)).thenReturn(success( + new AdminUserRespDTO().setId(userId).setDeptId(deptId))); + + // 调用 + Set permissionIds = permissionService.getReadableContentPermissionIdSet( + Arrays.asList(adminLibraryId, collaboratorLibraryId, deniedLibraryId), userId); + + // 断言 + assertEquals(new LinkedHashSet<>(Arrays.asList(adminPermission.getId(), collaboratorPermission.getId())), + permissionIds); + verify(libraryMemberService, never()).isLibraryAdmin(anyLong(), anyLong()); + } + + @Test + public void testDeleteUnusedPermissions_preservesReferencedPermission() { + // mock 数据 + Long libraryId = randomLongId(); + PmsKnowledgeContentPermissionDO referenced = randomPermissionDO(libraryId, true, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(referenced); + PmsKnowledgeContentPermissionDO unused = randomPermissionDO(libraryId, true, + PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + permissionMapper.insert(unused); + folderMapper.insert(new PmsKnowledgeFolderDO().setLibraryId(libraryId).setPermissionId(referenced.getId()) + .setParentId(0L).setTitle("测试目录").setStatus(1)); + when(folderService.getExistingContentPermissionIdSet(anyCollection())) + .thenReturn(Collections.singleton(referenced.getId())); + when(documentService.getExistingContentPermissionIdSet(anyCollection())) + .thenReturn(Collections.emptySet()); + + // 调用 + permissionService.deleteUnusedContentPermissions(new LinkedHashSet<>( + Arrays.asList(referenced.getId(), unused.getId()))); + + // 断言 + assertNotNull(permissionMapper.selectById(referenced.getId())); + assertNull(permissionMapper.selectById(unused.getId())); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeContentPermissionDO randomPermissionDO(Long libraryId, boolean openStatus, + Integer openLevel) { + return randomPojo(PmsKnowledgeContentPermissionDO.class, permission -> permission.setId(null) + .setLibraryId(libraryId).setOpenStatus(openStatus).setOpenLevel(openLevel) + .setCreator(String.valueOf(randomLongId()))); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelServiceImplTest.java new file mode 100644 index 000000000..daa10a767 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentLabelServiceImplTest.java @@ -0,0 +1,158 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.label.PmsKnowledgeDocumentLabelSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentLabelDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentLabelMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_LABEL_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeDocumentLabelServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeDocumentLabelServiceImpl.class) +public class PmsKnowledgeDocumentLabelServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeDocumentLabelServiceImpl documentLabelService; + + @Resource + private PmsKnowledgeDocumentLabelMapper documentLabelMapper; + @Resource + private PmsKnowledgeDocumentMapper documentMapper; + + @MockitoBean + private PmsKnowledgeLibraryMemberService libraryMemberService; + @MockitoBean + private PmsKnowledgeContentPermissionService contentPermissionService; + + @Test + public void testCreateDocumentLabel_success() { + // 准备参数 + PmsKnowledgeDocumentLabelSaveReqVO reqVO = new PmsKnowledgeDocumentLabelSaveReqVO() + .setName(" 产品 ").setColor(" #409EFF "); + + // 调用 + Long id = documentLabelService.createDocumentLabel(reqVO); + + // 断言 + PmsKnowledgeDocumentLabelDO documentLabel = documentLabelMapper.selectById(id); + assertEquals("产品", documentLabel.getName()); + assertEquals("#409EFF", documentLabel.getColor()); + } + + @Test + public void testUpdateDocumentLabel_notExists() { + // 准备参数 + PmsKnowledgeDocumentLabelSaveReqVO reqVO = new PmsKnowledgeDocumentLabelSaveReqVO() + .setId(randomLongId()).setName("产品").setColor("#409EFF"); + + // 调用,并断言异常 + assertServiceException(() -> documentLabelService.updateDocumentLabel(reqVO), + KNOWLEDGE_DOCUMENT_LABEL_NOT_EXISTS); + } + + @Test + public void testDeleteDocumentLabel_success() { + // mock 数据 + PmsKnowledgeDocumentLabelDO documentLabel = randomDocumentLabelDO(); + documentLabelMapper.insert(documentLabel); + + // 调用 + documentLabelService.deleteDocumentLabel(documentLabel.getId()); + + // 断言 + assertNull(documentLabelMapper.selectById(documentLabel.getId())); + } + + @Test + public void testGetDocumentLabel_success() { + // mock 数据 + PmsKnowledgeDocumentLabelDO documentLabel = randomDocumentLabelDO(); + documentLabelMapper.insert(documentLabel); + + // 调用 + PmsKnowledgeDocumentLabelDO result = documentLabelService.getDocumentLabel(documentLabel.getId()); + + // 断言 + assertEquals(documentLabel.getId(), result.getId()); + assertEquals(documentLabel.getName(), result.getName()); + assertEquals(documentLabel.getColor(), result.getColor()); + } + + @Test + public void testGetDocumentPageByLabel_success() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + PmsKnowledgeDocumentLabelDO targetLabel = randomDocumentLabelDO(); + documentLabelMapper.insert(targetLabel); + PmsKnowledgeDocumentLabelDO otherLabel = randomDocumentLabelDO(); + documentLabelMapper.insert(otherLabel); + PmsKnowledgeDocumentDO targetDocument = randomDocumentDO(libraryId, + Arrays.asList(targetLabel.getId(), otherLabel.getId())); + PmsKnowledgeDocumentDO otherDocument = randomDocumentDO(libraryId, + Collections.singletonList(otherLabel.getId())); + PmsKnowledgeDocumentDO unreadableDocument = randomDocumentDO(libraryId, + Collections.singletonList(targetLabel.getId())); + documentMapper.insert(targetDocument); + documentMapper.insert(otherDocument); + documentMapper.insert(unreadableDocument); + when(libraryMemberService.getReadableLibraryIdList(userId)).thenReturn(Collections.singletonList(libraryId)); + when(contentPermissionService.getReadableContentPermissionIdSet(Collections.singletonList(libraryId), userId)) + .thenReturn(new HashSet<>(Arrays.asList(targetDocument.getPermissionId(), otherDocument.getPermissionId()))); + // 准备参数 + PmsKnowledgeDocumentLabelPageReqVO pageReqVO = new PmsKnowledgeDocumentLabelPageReqVO() + .setLabelId(targetLabel.getId()); + pageReqVO.setPageNo(1).setPageSize(10); + + // 调用 + PageResult pageResult = + documentLabelService.getDocumentPageByLabel(pageReqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(targetLabel.getId(), CollUtil.getFirst(CollUtil.getFirst(pageResult.getList()).getLabelIds())); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeDocumentLabelDO randomDocumentLabelDO() { + return randomPojo(PmsKnowledgeDocumentLabelDO.class, + documentLabel -> documentLabel.setId(null).setName("产品").setColor("#409EFF")); + } + + private PmsKnowledgeDocumentDO randomDocumentDO(Long libraryId, List labelIds) { + return randomPojo(PmsKnowledgeDocumentDO.class, document -> document.setId(null).setLibraryId(libraryId) + .setPermissionId(randomLongId()) + .setFolderId(0L).setParentId(0L).setTitle("测试文档") + .setType(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType()) + .setStatus(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()).setLabelIds(labelIds)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentServiceImplTest.java new file mode 100644 index 000000000..08bb98692 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeDocumentServiceImplTest.java @@ -0,0 +1,498 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentSearchPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeFolderMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMemberMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentCommentService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentLikeService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentShareService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeGroupService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryService; +import cn.iocoder.yudao.module.pms.service.kb.recycle.PmsKnowledgeRecycleServiceImpl; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_MOVE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_MOVE_TARGET_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeDocumentServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import({PmsKnowledgeDocumentServiceImpl.class, PmsKnowledgeRecycleServiceImpl.class}) +public class PmsKnowledgeDocumentServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeDocumentServiceImpl documentService; + + @Resource + private PmsKnowledgeLibraryMapper libraryMapper; + @Resource + private PmsKnowledgeLibraryMemberMapper memberMapper; + @Resource + private PmsKnowledgeFolderMapper folderMapper; + @Resource + private PmsKnowledgeDocumentMapper documentMapper; + @MockitoBean + private PermissionApi permissionApi; + @MockitoBean + private AdminUserApi adminUserApi; + @MockitoBean + private PmsKnowledgeLibraryService libraryService; + @MockitoBean + private PmsKnowledgeLibraryMemberService memberService; + @MockitoBean + private PmsKnowledgeGroupService knowledgeGroupService; + @MockitoBean + private PmsKnowledgeDocumentShareService knowledgeDocumentShareService; + @MockitoBean + private PmsKnowledgeDocumentCommentService knowledgeDocumentCommentService; + @MockitoBean + private PmsKnowledgeContentPermissionService contentPermissionService; + @MockitoBean + private PmsKnowledgeDocumentLabelService documentLabelService; + @MockitoBean + private PmsKnowledgeFolderService folderService; + @MockitoBean + private PmsKnowledgeFavoriteService favoriteService; + @MockitoBean + private PmsKnowledgeDocumentLikeService documentLikeService; + @MockitoBean + private PmsKnowledgeViewRecordService viewRecordService; + + @Test + public void testCreateDocument_folderWinsParent() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + PmsKnowledgeFolderDO folder = randomFolderDO(library.getId(), 0L); + folderMapper.insert(folder); + PmsKnowledgeDocumentDO parent = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(parent); + when(folderService.getFolder(folder.getId(), userId)).thenReturn(folder); + // 准备参数 + PmsKnowledgeDocumentCreateReqVO reqVO = new PmsKnowledgeDocumentCreateReqVO() + .setLibraryId(library.getId()).setFolderId(folder.getId()).setParentId(parent.getId()) + .setTitle("说明文档").setType(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType()); + + // 调用 + Long documentId = documentService.createDocument(reqVO, userId); + + // 断言 + PmsKnowledgeDocumentDO document = documentMapper.selectById(documentId); + assertEquals(folder.getId(), document.getFolderId()); + assertEquals(0L, document.getParentId()); + assertEquals(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType(), document.getType()); + assertEquals(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus(), document.getStatus()); + verify(folderService).getFolder(folder.getId(), userId); + } + + @Test + public void testCreateDocument_fileDocument() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(folderService.getFolder(0L, userId)).thenReturn(null); + // 准备参数 + PmsKnowledgeDocumentCreateReqVO reqVO = new PmsKnowledgeDocumentCreateReqVO() + .setLibraryId(library.getId()).setFolderId(0L).setParentId(0L).setTitle("产品附件") + .setType(PmsKnowledgeDocumentTypeEnum.FILE.getType()).setContent("https://example.com/a.pdf") + .setFileType("pdf").setFileSize(2048L); + + // 调用 + Long documentId = documentService.createDocument(reqVO, userId); + + // 断言 + PmsKnowledgeDocumentDO document = documentMapper.selectById(documentId); + assertEquals(PmsKnowledgeDocumentTypeEnum.FILE.getType(), document.getType()); + assertEquals("https://example.com/a.pdf", document.getContent()); + assertEquals("pdf", document.getFileType()); + assertEquals(2048L, document.getFileSize()); + } + + @Test + public void testUpdateDocument_success() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId(), 0L, 0L).setContent("旧正文"); + documentMapper.insert(document); + // 准备参数 + PmsKnowledgeDocumentUpdateReqVO reqVO = new PmsKnowledgeDocumentUpdateReqVO() + .setId(document.getId()).setContent("新正文"); + + // 调用 + documentService.updateDocument(reqVO, userId); + + // 断言 + PmsKnowledgeDocumentDO updatedDocument = documentMapper.selectById(document.getId()); + assertEquals("新正文", updatedDocument.getContent()); + assertEquals(document.getTitle(), updatedDocument.getTitle()); + assertEquals(document.getType(), updatedDocument.getType()); + assertEquals(document.getStatus(), updatedDocument.getStatus()); + verify(memberService).validateLibraryReadable(document.getLibraryId(), userId); + verify(contentPermissionService).validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId); + } + + @Test + public void testUpdateDocument_titleAndContent() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId(), 0L, 0L).setContent("旧正文"); + documentMapper.insert(document); + // 准备参数 + PmsKnowledgeDocumentUpdateReqVO reqVO = new PmsKnowledgeDocumentUpdateReqVO() + .setId(document.getId()).setTitle("新标题").setContent("新正文"); + + // 调用 + documentService.updateDocument(reqVO, userId); + + // 断言 + PmsKnowledgeDocumentDO updatedDocument = documentMapper.selectById(document.getId()); + assertEquals("新标题", updatedDocument.getTitle()); + assertEquals("新正文", updatedDocument.getContent()); + } + + @Test + public void testDeleteDocument_recursive() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + PmsKnowledgeDocumentDO parent = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(parent); + PmsKnowledgeDocumentDO child = randomDocumentDO(library.getId(), 0L, parent.getId()); + documentMapper.insert(child); + + // 调用 + documentService.deleteDocument(parent.getId(), userId); + + // 断言 + assertEquals(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus(), + documentMapper.selectById(parent.getId()).getStatus()); + assertEquals(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus(), + documentMapper.selectById(child.getId()).getStatus()); + verify(memberService).validateLibraryReadable(library.getId(), userId); + } + + @Test + public void testMoveDocument_toDescendant() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + PmsKnowledgeDocumentDO parent = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(parent); + PmsKnowledgeDocumentDO child = randomDocumentDO(library.getId(), 0L, parent.getId()); + documentMapper.insert(child); + // 准备参数 + PmsKnowledgeDocumentMoveReqVO reqVO = new PmsKnowledgeDocumentMoveReqVO().setId(parent.getId()) + .setTargetLibraryId(library.getId()).setTargetFolderId(0L).setTargetParentId(child.getId()); + + // 调用,并断言异常 + assertServiceException(() -> documentService.moveDocument(reqVO, userId), KNOWLEDGE_DOCUMENT_MOVE_INVALID); + } + + @Test + public void testMoveDocument_targetFolderAndParentBothSet() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(document); + // 准备参数 + PmsKnowledgeDocumentMoveReqVO reqVO = new PmsKnowledgeDocumentMoveReqVO().setId(document.getId()) + .setTargetLibraryId(library.getId()).setTargetFolderId(randomLongId()).setTargetParentId(randomLongId()); + + // 调用,并断言异常 + assertServiceException(() -> documentService.moveDocument(reqVO, userId), KNOWLEDGE_DOCUMENT_MOVE_TARGET_INVALID); + } + + @Test + public void testMoveDocument_crossLibraryClonesPermission() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO sourceLibrary = randomLibraryDO(); + libraryMapper.insert(sourceLibrary); + memberMapper.insert(randomMemberDO(sourceLibrary.getId(), userId)); + when(memberService.validateLibraryReadable(sourceLibrary.getId(), userId)).thenReturn(sourceLibrary); + when(memberService.validateLibraryWritable(sourceLibrary.getId(), userId)).thenReturn(sourceLibrary); + PmsKnowledgeLibraryDO targetLibrary = randomLibraryDO(); + libraryMapper.insert(targetLibrary); + memberMapper.insert(randomMemberDO(targetLibrary.getId(), userId)); + when(memberService.validateLibraryReadable(targetLibrary.getId(), userId)).thenReturn(targetLibrary); + when(memberService.validateLibraryWritable(targetLibrary.getId(), userId)).thenReturn(targetLibrary); + PmsKnowledgeDocumentDO document = randomDocumentDO(sourceLibrary.getId(), 0L, 0L); + documentMapper.insert(document); + Long sourcePermissionId = document.getPermissionId(); + Long targetPermissionId = randomLongId(); + // 准备参数 + PmsKnowledgeDocumentMoveReqVO reqVO = new PmsKnowledgeDocumentMoveReqVO().setId(document.getId()) + .setTargetLibraryId(targetLibrary.getId()).setTargetFolderId(0L).setTargetParentId(0L); + // mock 方法 + when(contentPermissionService.cloneContentPermissions(Collections.singleton(sourcePermissionId), targetLibrary.getId())) + .thenReturn(Collections.singletonMap(sourcePermissionId, targetPermissionId)); + + // 调用 + documentService.moveDocument(reqVO, userId); + + // 断言 + PmsKnowledgeDocumentDO movedDocument = documentMapper.selectById(document.getId()); + assertEquals(targetLibrary.getId(), movedDocument.getLibraryId()); + assertEquals(targetPermissionId, movedDocument.getPermissionId()); + verify(memberService).validateLibraryReadable(sourceLibrary.getId(), userId); + verify(memberService).validateLibraryReadable(targetLibrary.getId(), userId); + verify(memberService).validateLibraryAdmin(targetLibrary.getId(), userId); + verify(contentPermissionService).validateContentPermissionManageable( + sourcePermissionId, sourceLibrary.getId(), userId); + verify(favoriteService).updateFavoriteLibraryIdByEntityIds(Collections.emptySet(), + Collections.singleton(document.getId()), targetLibrary.getId()); + verify(viewRecordService).updateViewRecordLibraryIdByEntityIds(Collections.emptySet(), + Collections.singleton(document.getId()), targetLibrary.getId()); + verify(contentPermissionService).deleteUnusedContentPermissions(Collections.singleton(sourcePermissionId)); + } + + @Test + public void testGetDocumentList_hidesChildWhenParentInvisible() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO parent = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(parent); + PmsKnowledgeDocumentDO child = randomDocumentDO(library.getId(), 0L, parent.getId()); + documentMapper.insert(child); + + // mock 方法:只有子文档可读,父文档不可读 + when(contentPermissionService.getCurrentUserContentPermissionLevelMap( + anyCollection(), eq(library.getId()), eq(userId))) + .thenReturn(Collections.singletonMap(child.getPermissionId(), 3)); + + // 调用并断言 + assertTrue(documentService.getDocumentList(library.getId(), userId).isEmpty()); + verify(memberService).validateLibraryReadable(library.getId(), userId); + } + + @Test + public void testGetDocument_success() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(document); + + // 调用 + PmsKnowledgeDocumentDO foundDocument = documentService.getDocument(document.getId(), userId); + + // 断言 + assertEquals(document.getId(), foundDocument.getId()); + verify(contentPermissionService).validateContentPermissionReadable(document.getPermissionId(), library.getId(), userId); + } + + @Test + public void testGetDocument_recycled() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(document); + documentMapper.updateById(new PmsKnowledgeDocumentDO().setId(document.getId()) + .setStatus(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> documentService.getDocument(document.getId(), userId), + KNOWLEDGE_DOCUMENT_NOT_EXISTS); + } + + @Test + public void testGetDocumentTypeCountMap_success() { + // mock 数据 + Long libraryId = randomLongId(); + Long otherLibraryId = randomLongId(); + documentMapper.insert(randomDocumentDO(libraryId, 0L, 0L)); + documentMapper.insert(randomDocumentDO(libraryId, 0L, 0L)); + documentMapper.insert(randomDocumentDO(libraryId, 0L, 0L) + .setType(PmsKnowledgeDocumentTypeEnum.FILE.getType())); + documentMapper.insert(randomDocumentDO(libraryId, 0L, 0L) + .setStatus(PmsKnowledgeDocumentStatusEnum.RECYCLED.getStatus())); + documentMapper.insert(randomDocumentDO(otherLibraryId, 0L, 0L)); + + // 调用 + Map> typeCountMap = documentService.getDocumentTypeCountMap( + Arrays.asList(libraryId, otherLibraryId)); + + // 断言 + assertEquals(2L, typeCountMap.get(libraryId).get(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType())); + assertEquals(1L, typeCountMap.get(libraryId).get(PmsKnowledgeDocumentTypeEnum.FILE.getType())); + assertEquals(1L, typeCountMap.get(otherLibraryId).get(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType())); + } + + @Test + public void testGetDocumentSearchPage_mysqlAndAccessScope() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + Long otherLibraryId = randomLongId(); + Long readablePermissionId = randomLongId(); + documentMapper.insert(randomSearchDocumentDO(libraryId, readablePermissionId, "产品说明", "普通正文", 1)); + documentMapper.insert(randomSearchDocumentDO(libraryId, readablePermissionId, + "普通标题", "包含产品设计方案", 1)); + documentMapper.insert(randomSearchDocumentDO(libraryId, randomLongId(), "产品私密文档", "不可见", 1)); + documentMapper.insert(randomSearchDocumentDO(otherLibraryId, readablePermissionId, + "产品已删除文档", "回收站", -1)); + when(memberService.getReadableLibraryIdList(userId)).thenReturn(Arrays.asList(libraryId, otherLibraryId)); + when(contentPermissionService.getReadableContentPermissionIdSet(Arrays.asList(libraryId, otherLibraryId), userId)) + .thenReturn(Collections.singleton(readablePermissionId)); + // 准备参数 + PmsKnowledgeDocumentSearchPageReqVO pageReqVO = new PmsKnowledgeDocumentSearchPageReqVO() + .setKeyword("产品"); + pageReqVO.setPageNo(1).setPageSize(10); + + // 调用 + PageResult pageResult = documentService.getDocumentSearchPage(pageReqVO, userId); + + // 断言 + assertEquals(2, pageResult.getTotal()); + assertEquals("产品", pageReqVO.getKeyword()); + } + + @Test + public void testGetDocumentSearchPage_libraryDenied() { + // mock 数据 + Long userId = randomLongId(); + Long readableLibraryId = randomLongId(); + when(memberService.getReadableLibraryIdList(userId)) + .thenReturn(Collections.singletonList(readableLibraryId)); + // 准备参数 + PmsKnowledgeDocumentSearchPageReqVO pageReqVO = new PmsKnowledgeDocumentSearchPageReqVO() + .setKeyword("产品").setLibraryId(randomLongId()); + + // 调用 + PageResult pageResult = documentService.getDocumentSearchPage(pageReqVO, userId); + + // 断言 + assertEquals(0, pageResult.getTotal()); + } + + @Test + public void testGetDocumentSearchPage_withoutKeyword() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + Long readablePermissionId = randomLongId(); + documentMapper.insert(randomSearchDocumentDO(libraryId, readablePermissionId, "产品说明", "普通正文", 1)); + documentMapper.insert(randomSearchDocumentDO(libraryId, readablePermissionId, "研发规范", "技术方案", 1)); + when(memberService.getReadableLibraryIdList(userId)).thenReturn(Collections.singletonList(libraryId)); + when(contentPermissionService.getReadableContentPermissionIdSet( + Collections.singletonList(libraryId), userId)) + .thenReturn(Collections.singleton(readablePermissionId)); + // 准备参数 + PmsKnowledgeDocumentSearchPageReqVO pageReqVO = new PmsKnowledgeDocumentSearchPageReqVO(); + pageReqVO.setPageNo(1).setPageSize(10); + + // 调用 + PageResult pageResult = documentService.getDocumentSearchPage(pageReqVO, userId); + + // 断言:空关键词按当前产品语义返回可读文档 + assertEquals(2, pageResult.getTotal()); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeLibraryDO randomLibraryDO() { + return randomPojo(PmsKnowledgeLibraryDO.class, library -> library.setId(null).setName("测试知识库") + .setDescription("测试").setOpenStatus(false)); + } + + private PmsKnowledgeLibraryMemberDO randomMemberDO(Long libraryId, Long userId) { + return randomPojo(PmsKnowledgeLibraryMemberDO.class, member -> member.setId(null).setLibraryId(libraryId) + .setUserId(userId).setLevel(PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + } + + private PmsKnowledgeFolderDO randomFolderDO(Long libraryId, Long parentId) { + return randomPojo(PmsKnowledgeFolderDO.class, folder -> folder.setId(null).setLibraryId(libraryId) + .setPermissionId(randomLongId()).setParentId(parentId).setTitle("测试目录").setStatus(1)); + } + + private PmsKnowledgeDocumentDO randomDocumentDO(Long libraryId, Long folderId, Long parentId) { + return randomPojo(PmsKnowledgeDocumentDO.class, document -> document.setId(null).setLibraryId(libraryId) + .setPermissionId(randomLongId()).setFolderId(folderId).setParentId(parentId).setTitle("测试文档") + .setType(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType()) + .setStatus(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus())); + } + + private PmsKnowledgeDocumentDO randomSearchDocumentDO(Long libraryId, Long permissionId, String title, + String content, Integer status) { + return randomPojo(PmsKnowledgeDocumentDO.class, document -> document.setId(null).setLibraryId(libraryId) + .setPermissionId(permissionId) + .setFolderId(0L).setParentId(0L).setTitle(title).setContent(content) + .setType(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType()).setStatus(status)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderServiceImplTest.java new file mode 100644 index 000000000..d990a04d0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/content/PmsKnowledgeFolderServiceImplTest.java @@ -0,0 +1,247 @@ +package cn.iocoder.yudao.module.pms.service.kb.content; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.folder.PmsKnowledgeFolderSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeFolderMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMemberMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentCommentService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentLikeService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentShareService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeGroupService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryService; +import cn.iocoder.yudao.module.pms.service.kb.recycle.PmsKnowledgeRecycleServiceImpl; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_FOLDER_MOVE_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeFolderServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import({PmsKnowledgeFolderServiceImpl.class, PmsKnowledgeRecycleServiceImpl.class}) +public class PmsKnowledgeFolderServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeFolderServiceImpl folderService; + + @Resource + private PmsKnowledgeLibraryMapper libraryMapper; + @Resource + private PmsKnowledgeLibraryMemberMapper memberMapper; + @Resource + private PmsKnowledgeFolderMapper folderMapper; + @Resource + private PmsKnowledgeDocumentMapper documentMapper; + + @MockitoBean + private PermissionApi permissionApi; + @MockitoBean + private AdminUserApi adminUserApi; + @MockitoBean + private PmsKnowledgeLibraryService libraryService; + @MockitoBean + private PmsKnowledgeLibraryMemberService memberService; + @MockitoBean + private PmsKnowledgeGroupService knowledgeGroupService; + @MockitoBean + private PmsKnowledgeDocumentShareService knowledgeDocumentShareService; + @MockitoBean + private PmsKnowledgeDocumentCommentService knowledgeDocumentCommentService; + @MockitoBean + private PmsKnowledgeContentPermissionService contentPermissionService; + @MockitoBean + private PmsKnowledgeDocumentService documentService; + @MockitoBean + private PmsKnowledgeFavoriteService favoriteService; + @MockitoBean + private PmsKnowledgeDocumentLikeService documentLikeService; + @MockitoBean + private PmsKnowledgeViewRecordService viewRecordService; + + @Test + public void testCreateFolder_success() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + when(contentPermissionService.createDefaultContentPermission(library.getId(), userId)).thenReturn(randomLongId()); + // 准备参数 + PmsKnowledgeFolderSaveReqVO reqVO = new PmsKnowledgeFolderSaveReqVO() + .setLibraryId(library.getId()).setParentId(0L).setTitle(" 产品文档 "); + + // 调用 + Long folderId = folderService.createFolder(reqVO, userId); + + // 断言 + PmsKnowledgeFolderDO folder = folderMapper.selectById(folderId); + assertEquals(library.getId(), folder.getLibraryId()); + assertEquals(" 产品文档 ", folder.getTitle()); + assertEquals(1, folder.getStatus()); + } + + @Test + public void testMoveFolder_toDescendant() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + PmsKnowledgeFolderDO parent = randomFolderDO(library.getId(), 0L); + folderMapper.insert(parent); + PmsKnowledgeFolderDO child = randomFolderDO(library.getId(), parent.getId()); + folderMapper.insert(child); + // 准备参数 + PmsKnowledgeFolderMoveReqVO reqVO = new PmsKnowledgeFolderMoveReqVO().setId(parent.getId()) + .setTargetLibraryId(library.getId()).setTargetParentId(child.getId()); + + // 调用,并断言异常 + assertServiceException(() -> folderService.moveFolder(reqVO, userId), KNOWLEDGE_FOLDER_MOVE_INVALID); + } + + @Test + public void testMoveFolder_crossLibraryUpdatesInteractionLibraryId() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO sourceLibrary = randomLibraryDO(); + libraryMapper.insert(sourceLibrary); + PmsKnowledgeLibraryDO targetLibrary = randomLibraryDO(); + libraryMapper.insert(targetLibrary); + when(memberService.validateLibraryReadable(sourceLibrary.getId(), userId)).thenReturn(sourceLibrary); + when(memberService.validateLibraryReadable(targetLibrary.getId(), userId)).thenReturn(targetLibrary); + when(memberService.validateLibraryWritable(targetLibrary.getId(), userId)).thenReturn(targetLibrary); + PmsKnowledgeFolderDO parent = randomFolderDO(sourceLibrary.getId(), 0L); + folderMapper.insert(parent); + PmsKnowledgeFolderDO child = randomFolderDO(sourceLibrary.getId(), parent.getId()); + folderMapper.insert(child); + PmsKnowledgeDocumentDO document = randomDocumentDO(sourceLibrary.getId(), child.getId(), 0L) + .setId(randomLongId()); + when(documentService.getDocumentListByFolderIds(anyCollection())).thenReturn(Arrays.asList(document)); + Map clonedPermissionIdMap = new HashMap<>(); + clonedPermissionIdMap.put(parent.getPermissionId(), randomLongId()); + clonedPermissionIdMap.put(child.getPermissionId(), randomLongId()); + clonedPermissionIdMap.put(document.getPermissionId(), randomLongId()); + when(contentPermissionService.cloneContentPermissions(anyCollection(), eq(targetLibrary.getId()))) + .thenReturn(clonedPermissionIdMap); + // 准备参数 + PmsKnowledgeFolderMoveReqVO reqVO = new PmsKnowledgeFolderMoveReqVO().setId(parent.getId()) + .setTargetLibraryId(targetLibrary.getId()).setTargetParentId(0L); + + // 调用 + folderService.moveFolder(reqVO, userId); + + // 断言 + Set folderIds = new HashSet<>(Arrays.asList(parent.getId(), child.getId())); + Set documentIds = Collections.singleton(document.getId()); + assertEquals(targetLibrary.getId(), folderMapper.selectById(parent.getId()).getLibraryId()); + assertEquals(targetLibrary.getId(), folderMapper.selectById(child.getId()).getLibraryId()); + verify(memberService).validateLibraryAdmin(targetLibrary.getId(), userId); + verify(contentPermissionService).validateContentPermissionManageable( + parent.getPermissionId(), sourceLibrary.getId(), userId); + verify(favoriteService).updateFavoriteLibraryIdByEntityIds(folderIds, documentIds, targetLibrary.getId()); + verify(viewRecordService).updateViewRecordLibraryIdByEntityIds(folderIds, documentIds, + targetLibrary.getId()); + } + + @Test + public void testDeleteFolder_recursive() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + memberMapper.insert(randomMemberDO(library.getId(), userId)); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.validateLibraryWritable(library.getId(), userId)).thenReturn(library); + PmsKnowledgeFolderDO parentFolder = randomFolderDO(library.getId(), 0L); + folderMapper.insert(parentFolder); + PmsKnowledgeFolderDO childFolder = randomFolderDO(library.getId(), parentFolder.getId()); + folderMapper.insert(childFolder); + PmsKnowledgeDocumentDO parentDocument = randomDocumentDO(library.getId(), childFolder.getId(), 0L); + documentMapper.insert(parentDocument); + PmsKnowledgeDocumentDO childDocument = randomDocumentDO(library.getId(), 0L, parentDocument.getId()); + documentMapper.insert(childDocument); + when(documentService.getDocumentListByFolderIds(anyCollection())) + .thenReturn(Arrays.asList(parentDocument, childDocument)); + doAnswer(invocation -> { + List documents = invocation.getArgument(0); + documentMapper.updateBatch(documents); + return null; + }).when(documentService).updateDocumentList(anyCollection()); + + // 调用 + folderService.deleteFolder(parentFolder.getId(), userId); + + // 断言 + assertEquals(-1, folderMapper.selectById(parentFolder.getId()).getStatus()); + assertEquals(-1, folderMapper.selectById(childFolder.getId()).getStatus()); + assertEquals(-1, documentMapper.selectById(parentDocument.getId()).getStatus()); + assertEquals(-1, documentMapper.selectById(childDocument.getId()).getStatus()); + verify(memberService).validateLibraryReadable(library.getId(), userId); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeLibraryDO randomLibraryDO() { + return randomPojo(PmsKnowledgeLibraryDO.class, library -> library.setId(null).setName("测试知识库") + .setDescription("测试").setOpenStatus(false)); + } + + private PmsKnowledgeLibraryMemberDO randomMemberDO(Long libraryId, Long userId) { + return randomPojo(PmsKnowledgeLibraryMemberDO.class, member -> member.setId(null).setLibraryId(libraryId) + .setUserId(userId).setLevel(PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + } + + private PmsKnowledgeFolderDO randomFolderDO(Long libraryId, Long parentId) { + return randomPojo(PmsKnowledgeFolderDO.class, folder -> folder.setId(null).setLibraryId(libraryId) + .setPermissionId(randomLongId()).setParentId(parentId).setTitle("测试目录").setStatus(1)); + } + + private PmsKnowledgeDocumentDO randomDocumentDO(Long libraryId, Long folderId, Long parentId) { + return randomPojo(PmsKnowledgeDocumentDO.class, document -> document.setId(null).setLibraryId(libraryId) + .setPermissionId(randomLongId()).setFolderId(folderId).setParentId(parentId).setTitle("测试文档") + .setType(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType()) + .setStatus(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentServiceImplTest.java new file mode 100644 index 000000000..0425aca6a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentCommentServiceImplTest.java @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.comment.PmsKnowledgeDocumentCommentSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentCommentDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeDocumentCommentMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_COMMENT_REPLY_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_COMMENT_TYPE_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeDocumentCommentServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeDocumentCommentServiceImpl.class) +public class PmsKnowledgeDocumentCommentServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeDocumentCommentServiceImpl commentService; + + @Resource + private PmsKnowledgeDocumentCommentMapper commentMapper; + + @MockitoBean + private PmsKnowledgeDocumentService documentService; + + @Test + public void testCreateComment_reply() { + // mock 数据 + Long documentId = randomLongId(); + Long userId = randomLongId(); + mockRichTextDocument(documentId); + Long mainId = commentService.createDocumentComment(new PmsKnowledgeDocumentCommentSaveReqVO() + .setDocumentId(documentId).setContent("主评论"), userId); + + // 准备参数 + PmsKnowledgeDocumentCommentSaveReqVO reqVO = new PmsKnowledgeDocumentCommentSaveReqVO() + .setDocumentId(documentId).setMainId(mainId).setReplyUserId(userId).setContent("回复内容"); + + // 调用 + Long replyId = commentService.createDocumentComment(reqVO, randomLongId()); + + // 断言 + PmsKnowledgeDocumentCommentDO reply = commentMapper.selectById(replyId); + assertEquals(mainId, reply.getMainId()); + assertEquals("回复内容", reply.getContent()); + } + + @Test + public void testCreateComment_replyDifferentDocument() { + // mock 数据 + Long mainDocumentId = randomLongId(); + Long targetDocumentId = randomLongId(); + mockRichTextDocument(mainDocumentId); + mockRichTextDocument(targetDocumentId); + Long mainId = commentService.createDocumentComment(new PmsKnowledgeDocumentCommentSaveReqVO() + .setDocumentId(mainDocumentId).setContent("主评论"), randomLongId()); + + // 准备参数 + PmsKnowledgeDocumentCommentSaveReqVO reqVO = new PmsKnowledgeDocumentCommentSaveReqVO() + .setDocumentId(targetDocumentId).setMainId(mainId).setContent("非法回复"); + + // 调用并断言 + assertServiceException(() -> commentService.createDocumentComment(reqVO, randomLongId()), + KNOWLEDGE_DOCUMENT_COMMENT_REPLY_INVALID); + } + + @Test + public void testDeleteComment_deletesReplies() { + // mock 数据 + Long documentId = randomLongId(); + Long userId = randomLongId(); + mockRichTextDocument(documentId); + Long mainId = commentService.createDocumentComment(new PmsKnowledgeDocumentCommentSaveReqVO() + .setDocumentId(documentId).setContent("主评论"), userId); + commentService.createDocumentComment(new PmsKnowledgeDocumentCommentSaveReqVO().setDocumentId(documentId) + .setMainId(mainId).setReplyUserId(userId).setContent("回复"), randomLongId()); + + // 调用 + commentService.deleteDocumentComment(mainId, userId); + + // 断言 + assertTrue(CollUtil.isEmpty(commentMapper.selectListByDocumentId(documentId))); + } + + @Test + public void testGetCommentList() { + // mock 数据 + Long documentId = randomLongId(); + mockRichTextDocument(documentId); + commentService.createDocumentComment(new PmsKnowledgeDocumentCommentSaveReqVO() + .setDocumentId(documentId).setContent("第一条"), randomLongId()); + commentService.createDocumentComment(new PmsKnowledgeDocumentCommentSaveReqVO() + .setDocumentId(documentId).setContent("第二条"), randomLongId()); + + // 调用 + List comments = commentService.getDocumentCommentList(documentId, randomLongId()); + + // 断言 + assertEquals(2, comments.size()); + } + + @Test + public void testCreateComment_fileDocument() { + // mock 数据 + Long documentId = randomLongId(); + Long userId = randomLongId(); + when(documentService.getDocument(documentId, userId)).thenReturn(new PmsKnowledgeDocumentDO() + .setId(documentId).setType(PmsKnowledgeDocumentTypeEnum.FILE.getType())); + + // 准备参数 + PmsKnowledgeDocumentCommentSaveReqVO reqVO = new PmsKnowledgeDocumentCommentSaveReqVO() + .setDocumentId(documentId).setContent("文件评论"); + + // 调用并断言 + assertServiceException(() -> commentService.createDocumentComment(reqVO, userId), + KNOWLEDGE_DOCUMENT_COMMENT_TYPE_INVALID); + } + + // ========== Mock 方法 ========== + + private void mockRichTextDocument(Long documentId) { + when(documentService.getDocument(eq(documentId), anyLong())).thenReturn(new PmsKnowledgeDocumentDO() + .setId(documentId).setType(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeServiceImplTest.java new file mode 100644 index 000000000..9e915a99b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentLikeServiceImplTest.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeDocumentLikeMapper; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Collections; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeDocumentLikeServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeDocumentLikeServiceImpl.class) +public class PmsKnowledgeDocumentLikeServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeDocumentLikeServiceImpl documentLikeService; + + @Resource + private PmsKnowledgeDocumentLikeMapper documentLikeMapper; + + @MockitoBean + private PmsKnowledgeInteractionTargetService interactionTargetService; + + @Test + public void testCreateDocumentLike_idempotent() { + // mock 数据 + Long userId = randomLongId(); + Long documentId = randomLongId(); + when(interactionTargetService.validateTargetReadable( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), documentId, userId)).thenReturn(randomLongId()); + + // 调用 + documentLikeService.createDocumentLike(documentId, userId); + documentLikeService.createDocumentLike(documentId, userId); + + // 断言 + assertNotNull(documentLikeMapper.selectByDocumentIdAndUserId(documentId, userId)); + } + + @Test + public void testDeleteDocumentLike_idempotent() { + // mock 数据 + Long userId = randomLongId(); + Long documentId = randomLongId(); + when(interactionTargetService.validateTargetReadable( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), documentId, userId)).thenReturn(randomLongId()); + documentLikeService.createDocumentLike(documentId, userId); + + // 调用 + documentLikeService.deleteDocumentLike(documentId, userId); + documentLikeService.deleteDocumentLike(documentId, userId); + + // 断言 + assertNull(documentLikeMapper.selectByDocumentIdAndUserId(documentId, userId)); + } + + @Test + public void testDeleteLikesByDocumentIds() { + // mock 数据 + Long userId = randomLongId(); + Long documentId = randomLongId(); + when(interactionTargetService.validateTargetReadable( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), documentId, userId)).thenReturn(randomLongId()); + documentLikeService.createDocumentLike(documentId, userId); + + // 调用 + documentLikeService.deleteLikesByDocumentIds(Collections.singleton(documentId)); + + // 断言 + assertTrue(CollUtil.isEmpty(documentLikeMapper.selectListByDocumentId(documentId))); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareServiceImplTest.java new file mode 100644 index 000000000..7e3d690c8 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeDocumentShareServiceImplTest.java @@ -0,0 +1,227 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeDocumentShareDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeDocumentShareMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMapper; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_SHARE_ALREADY_OPEN; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_SHARE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.MessageTemplateConstants.KNOWLEDGE_DOCUMENT_SHARED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeDocumentShareServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeDocumentShareServiceImpl.class) +public class PmsKnowledgeDocumentShareServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeDocumentShareServiceImpl documentShareService; + + @Resource + private PmsKnowledgeDocumentShareMapper documentShareMapper; + @Resource + private PmsKnowledgeDocumentMapper documentMapper; + @Resource + private PmsKnowledgeLibraryMapper libraryMapper; + + @MockitoBean + private PmsKnowledgeContentPermissionService contentPermissionService; + @MockitoBean + private NotifyMessageSendApi notifyMessageSendApi; + @MockitoBean + private AdminUserApi adminUserApi; + + @BeforeEach + public void setUpCloudApiMocks() { + lenient().when(adminUserApi.validateUserList(anyCollection())).thenReturn(success(true)); + lenient().when(notifyMessageSendApi.sendSingleMessageToAdmin(any())).thenReturn(success(randomLongId())); + } + + @Test + public void testOpenShare_duplicateAndReopen() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId()); + documentMapper.insert(document); + when(contentPermissionService.validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId)).thenReturn(2); + + // 调用开启分享,并断言重复开启异常 + PmsKnowledgeDocumentShareDO firstShare = documentShareService.openShare(document.getId(), + Arrays.asList(userId, userId), userId); + assertEquals(Collections.singletonList(userId), firstShare.getShareUserIds()); + assertServiceException(() -> documentShareService.openShare(document.getId(), null, userId), + KNOWLEDGE_DOCUMENT_SHARE_ALREADY_OPEN); + + // 调用关闭和重新开启,并断言令牌已更新 + documentShareService.closeShare(document.getId(), userId); + PmsKnowledgeDocumentShareDO reopenedShare = documentShareService.openShare(document.getId(), null, userId); + assertEquals(firstShare.getId(), reopenedShare.getId()); + assertNotEquals(firstShare.getToken(), reopenedShare.getToken()); + assertNull(reopenedShare.getCloseTime()); + } + + @Test + public void testUpdateShareMembers() { + // mock 数据 + Long userId = randomLongId(); + Long memberUserId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId()); + documentMapper.insert(document); + when(contentPermissionService.validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId)).thenReturn(2); + documentShareService.openShare(document.getId(), null, userId); + + // 调用 + documentShareService.updateShareMemberList(document.getId(), Collections.singletonList(memberUserId), userId); + + // 断言 + assertEquals(Collections.singletonList(memberUserId), + documentShareMapper.selectByDocumentId(document.getId()).getShareUserIds()); + ArgumentCaptor captor = ArgumentCaptor + .forClass(NotifySendSingleToUserReqDTO.class); + verify(notifyMessageSendApi).sendSingleMessageToAdmin(captor.capture()); + assertEquals(memberUserId, captor.getValue().getUserId()); + assertEquals(KNOWLEDGE_DOCUMENT_SHARED, captor.getValue().getTemplateCode()); + assertEquals(document.getTitle(), captor.getValue().getTemplateParams().get("documentTitle")); + String shareToken = documentShareMapper.selectByDocumentId(document.getId()).getToken(); + assertEquals("/pms/kb/document/share/" + shareToken, + captor.getValue().getTemplateParams().get("route")); + } + + @Test + public void testUpdateShareMembers_deduplicateAndValidateUsers() { + // mock 数据 + Long userId = randomLongId(); + Long memberUserId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId()); + documentMapper.insert(document); + when(contentPermissionService.validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId)).thenReturn(2); + documentShareService.openShare(document.getId(), null, userId); + + // 调用 + documentShareService.updateShareMemberList(document.getId(), + Arrays.asList(memberUserId, memberUserId), userId); + + // 断言:持久化和用户校验均使用去重后的名单 + List expectedUserIds = Collections.singletonList(memberUserId); + assertEquals(expectedUserIds, + documentShareMapper.selectByDocumentId(document.getId()).getShareUserIds()); + verify(adminUserApi).validateUserList(expectedUserIds); + } + + @Test + public void testGetDocumentByShareToken_invalidAfterRecycle() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId()); + documentMapper.insert(document); + when(contentPermissionService.validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId)).thenReturn(2); + PmsKnowledgeDocumentShareDO share = documentShareService.openShare(document.getId(), null, userId); + documentMapper.updateById(new PmsKnowledgeDocumentDO().setId(document.getId()).setStatus(-1)); + + // 调用并断言 + assertServiceException(() -> documentShareService.getDocumentByShareToken(share.getToken()), + KNOWLEDGE_DOCUMENT_SHARE_INVALID); + } + + @Test + public void testGetActiveDocumentShare_requiresContentReadable() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId()); + documentMapper.insert(document); + PmsKnowledgeDocumentShareDO share = new PmsKnowledgeDocumentShareDO().setDocumentId(document.getId()) + .setToken("share-token").setStatus(CommonStatusEnum.ENABLE.getStatus()); + documentShareMapper.insert(share); + when(contentPermissionService.validateContentPermissionReadable( + document.getPermissionId(), document.getLibraryId(), userId)).thenReturn(1); + + // 调用 + PmsKnowledgeDocumentShareDO result = documentShareService.getActiveDocumentShare(document.getId(), userId); + + // 断言 + assertEquals(share.getId(), result.getId()); + verify(contentPermissionService).validateContentPermissionReadable( + document.getPermissionId(), document.getLibraryId(), userId); + } + + @Test + public void testDeleteSharesByDocumentIds() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId()); + documentMapper.insert(document); + when(contentPermissionService.validateContentPermissionWritable( + document.getPermissionId(), document.getLibraryId(), userId)).thenReturn(2); + documentShareService.openShare(document.getId(), null, userId); + + // 调用 + documentShareService.deleteSharesByDocumentIds(Collections.singleton(document.getId())); + + // 断言 + assertNull(documentShareMapper.selectByDocumentId(document.getId())); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeLibraryDO randomLibraryDO() { + return randomPojo(PmsKnowledgeLibraryDO.class, library -> library.setId(null).setName("测试知识库") + .setOpenStatus(true).setStatus(1)); + } + + private PmsKnowledgeDocumentDO randomDocumentDO(Long libraryId) { + return randomPojo(PmsKnowledgeDocumentDO.class, document -> document.setId(null).setLibraryId(libraryId) + .setPermissionId(randomLongId()).setFolderId(0L).setParentId(0L).setTitle("测试文档") + .setType(3).setStatus(1)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteServiceImplTest.java new file mode 100644 index 000000000..bbaa46153 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeFavoriteServiceImplTest.java @@ -0,0 +1,189 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoritePageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.favorite.PmsKnowledgeFavoriteSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeFavoriteDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeFavoriteMapper; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_FAVORITE_DUPLICATE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeFavoriteServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeFavoriteServiceImpl.class) +public class PmsKnowledgeFavoriteServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeFavoriteServiceImpl favoriteService; + + @Resource + private PmsKnowledgeFavoriteMapper favoriteMapper; + + @MockitoBean + private PmsKnowledgeInteractionTargetService interactionTargetService; + + @Test + public void testCreateFavorite_duplicateAndDelete() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + PmsKnowledgeFavoriteSaveReqVO saveReqVO = new PmsKnowledgeFavoriteSaveReqVO() + .setType(PmsKnowledgeObjectTypeEnum.LIBRARY.getType()).setEntityId(libraryId); + when(interactionTargetService.validateTargetReadable(saveReqVO.getType(), libraryId, userId)) + .thenReturn(libraryId); + + // 调用创建,并断言重复关注异常 + favoriteService.createFavorite(saveReqVO, userId); + assertTrue(favoriteService.isFavorite(saveReqVO.getType(), libraryId, userId)); + assertServiceException(() -> favoriteService.createFavorite(saveReqVO, userId), + KNOWLEDGE_FAVORITE_DUPLICATE); + assertNotNull(favoriteMapper.selectByUserIdAndTypeAndEntityId(userId, saveReqVO.getType(), libraryId)); + + // 调用取消关注,并断言 + favoriteService.deleteFavorite(saveReqVO.getType(), libraryId, userId); + assertFalse(favoriteService.isFavorite(saveReqVO.getType(), libraryId, userId)); + assertNull(favoriteMapper.selectByUserIdAndTypeAndEntityId(userId, saveReqVO.getType(), libraryId)); + } + + @Test + public void testGetFavoritePage_filters() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeFavoriteDO firstFolder = randomFavoriteDO(userId, randomLongId(), + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), randomLongId()); + favoriteMapper.insert(firstFolder); + PmsKnowledgeFavoriteDO secondFolder = randomFavoriteDO(userId, randomLongId(), + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), randomLongId()); + favoriteMapper.insert(secondFolder); + PmsKnowledgeFavoriteDO document = randomFavoriteDO(userId, randomLongId(), + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), randomLongId()); + favoriteMapper.insert(document); + when(interactionTargetService.getReadableItemList(anyCollection(), eq(userId))).thenReturn(Arrays.asList( + new PmsKnowledgeInteractionItemRespVO().setId(firstFolder.getId()) + .setType(PmsKnowledgeObjectTypeEnum.FOLDER.getType()) + .setName("产品资料").setLibraryName("产品知识库"), + new PmsKnowledgeInteractionItemRespVO().setId(secondFolder.getId()) + .setType(PmsKnowledgeObjectTypeEnum.FOLDER.getType()) + .setName("设计资料").setLibraryName("产品知识库"), + new PmsKnowledgeInteractionItemRespVO().setId(document.getId()) + .setType(PmsKnowledgeObjectTypeEnum.DOCUMENT.getType()) + .setName("产品说明").setLibraryName("产品知识库"))); + PmsKnowledgeFavoritePageReqVO pageReqVO = new PmsKnowledgeFavoritePageReqVO() + .setType(PmsKnowledgeObjectTypeEnum.FOLDER.getType()); + pageReqVO.setPageNo(1); + pageReqVO.setPageSize(1); + + // 调用 + PageResult favoritePage = favoriteService.getFavoritePage(pageReqVO, + userId); + + // 断言 + assertEquals(2, favoritePage.getTotal()); + assertEquals(1, favoritePage.getList().size()); + assertEquals(PmsKnowledgeObjectTypeEnum.FOLDER.getType(), + CollUtil.getFirst(favoritePage.getList()).getType()); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetFavoriteListByLibraryId_filtersLibrary() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + favoriteMapper.insert(randomFavoriteDO(userId, libraryId, + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), randomLongId())); + favoriteMapper.insert(randomFavoriteDO(userId, randomLongId(), + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), randomLongId())); + when(interactionTargetService.getReadableItemList(anyCollection(), eq(userId))) + .thenAnswer(invocation -> (List) invocation.getArgument(0)); + + // 调用 + List favoriteList = favoriteService + .getFavoriteListByLibraryId(libraryId, userId); + + // 断言 + assertEquals(1, favoriteList.size()); + assertEquals(libraryId, CollUtil.getFirst(favoriteList).getLibraryId()); + } + + @Test + public void testDeleteFavoritesByEntityIds() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + Long folderId = randomLongId(); + Long documentId = randomLongId(); + favoriteMapper.insert(randomFavoriteDO(userId, libraryId, + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), folderId)); + favoriteMapper.insert(randomFavoriteDO(userId, libraryId, + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), documentId)); + + // 调用 + favoriteService.deleteFavoritesByEntityIds(Collections.singleton(folderId), + Collections.singleton(documentId)); + + // 断言 + assertTrue(CollUtil.isEmpty(favoriteMapper.selectListByUserId(userId))); + } + + @Test + public void testUpdateFavoriteLibraryIdByEntityIds() { + // mock 数据 + Long userId = randomLongId(); + Long sourceLibraryId = randomLongId(); + Long targetLibraryId = randomLongId(); + Long entityId = randomLongId(); + favoriteMapper.insert(randomFavoriteDO(userId, sourceLibraryId, + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), entityId)); + favoriteMapper.insert(randomFavoriteDO(userId, sourceLibraryId, + PmsKnowledgeObjectTypeEnum.FILE.getType(), entityId)); + favoriteMapper.insert(randomFavoriteDO(userId, sourceLibraryId, + PmsKnowledgeObjectTypeEnum.LIBRARY.getType(), entityId)); + + // 调用 + favoriteService.updateFavoriteLibraryIdByEntityIds(Collections.singleton(entityId), + Collections.singleton(entityId), targetLibraryId); + + // 断言 + assertEquals(targetLibraryId, favoriteMapper.selectByUserIdAndTypeAndEntityId(userId, + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), entityId).getLibraryId()); + assertEquals(targetLibraryId, favoriteMapper.selectByUserIdAndTypeAndEntityId(userId, + PmsKnowledgeObjectTypeEnum.FILE.getType(), entityId).getLibraryId()); + assertEquals(sourceLibraryId, favoriteMapper.selectByUserIdAndTypeAndEntityId(userId, + PmsKnowledgeObjectTypeEnum.LIBRARY.getType(), entityId).getLibraryId()); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeFavoriteDO randomFavoriteDO(Long userId, Long libraryId, Integer type, Long entityId) { + return randomPojo(PmsKnowledgeFavoriteDO.class, favorite -> favorite.setId(null).setLibraryId(libraryId) + .setType(type).setEntityId(entityId).setUserId(userId)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetServiceImplTest.java new file mode 100644 index 000000000..2fe5ea134 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeInteractionTargetServiceImplTest.java @@ -0,0 +1,101 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryService; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_INTERACTION_OBJECT_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeInteractionTargetServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +public class PmsKnowledgeInteractionTargetServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private PmsKnowledgeInteractionTargetServiceImpl interactionTargetService; + + @Mock + private PmsKnowledgeLibraryMemberService libraryMemberService; + @Mock + private PmsKnowledgeLibraryService libraryService; + @Mock + private PmsKnowledgeFolderReadService folderReadService; + @Mock + private PmsKnowledgeDocumentService documentService; + @Mock + private PmsKnowledgeContentPermissionService contentPermissionService; + + @Test + public void testGetReadableItemList_filtersContentPermission() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + Long readablePermissionId = randomLongId(); + Long unreadablePermissionId = randomLongId(); + PmsKnowledgeLibraryDO library = new PmsKnowledgeLibraryDO().setId(libraryId) + .setName("产品知识库").setStatus(1); + PmsKnowledgeFolderDO readableFolder = new PmsKnowledgeFolderDO().setId(randomLongId()) + .setLibraryId(libraryId).setPermissionId(readablePermissionId).setTitle("可读文件夹").setStatus(1); + PmsKnowledgeFolderDO unreadableFolder = new PmsKnowledgeFolderDO().setId(randomLongId()) + .setLibraryId(libraryId).setPermissionId(unreadablePermissionId).setTitle("不可读文件夹").setStatus(1); + when(libraryMemberService.getReadableLibraryIdList(userId)).thenReturn(Collections.singletonList(libraryId)); + when(libraryService.getLibraryList(Collections.singleton(libraryId))) + .thenReturn(Collections.singletonList(library)); + when(folderReadService.getFolderList(anyCollection())) + .thenReturn(Arrays.asList(readableFolder, unreadableFolder)); + when(documentService.getDocumentList(anyCollection())).thenReturn(Collections.emptyList()); + when(contentPermissionService.getReadableContentPermissionIdSet(Collections.singleton(libraryId), userId)) + .thenReturn(Collections.singleton(readablePermissionId)); + List targets = Arrays.asList( + new PmsKnowledgeInteractionItemRespVO().setId(randomLongId()) + .setType(PmsKnowledgeObjectTypeEnum.FOLDER.getType()) + .setEntityId(readableFolder.getId()).setLibraryId(libraryId), + new PmsKnowledgeInteractionItemRespVO().setId(randomLongId()) + .setType(PmsKnowledgeObjectTypeEnum.FOLDER.getType()) + .setEntityId(unreadableFolder.getId()).setLibraryId(libraryId)); + + // 调用 + List items = interactionTargetService.getReadableItemList(targets, userId); + + // 断言 + assertEquals(1, items.size()); + assertEquals(readableFolder.getId(), CollUtil.getFirst(items).getEntityId()); + } + + @Test + public void testValidateTargetReadable_documentTypeMismatch() { + // mock 数据 + Long userId = randomLongId(); + Long documentId = randomLongId(); + when(documentService.getDocument(documentId, userId)).thenReturn(new PmsKnowledgeDocumentDO() + .setId(documentId).setLibraryId(randomLongId()).setType(PmsKnowledgeObjectTypeEnum.FILE.getType())); + + // 调用,并断言 + assertServiceException(() -> interactionTargetService.validateTargetReadable( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), documentId, userId), + KNOWLEDGE_INTERACTION_OBJECT_INVALID); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordServiceImplTest.java new file mode 100644 index 000000000..91f04f185 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/interaction/PmsKnowledgeViewRecordServiceImplTest.java @@ -0,0 +1,158 @@ +package cn.iocoder.yudao.module.pms.service.kb.interaction; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.PmsKnowledgeInteractionItemRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.interaction.vo.viewrecord.PmsKnowledgeRecentListRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.interaction.PmsKnowledgeViewRecordDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.interaction.PmsKnowledgeViewRecordMapper; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeViewRecordServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeViewRecordServiceImpl.class) +public class PmsKnowledgeViewRecordServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeViewRecordServiceImpl viewRecordService; + + @Resource + private PmsKnowledgeViewRecordMapper viewRecordMapper; + + @MockitoBean + private PmsKnowledgeInteractionTargetService interactionTargetService; + @MockitoBean + private PmsKnowledgeLibraryMemberService libraryMemberService; + + @Test + public void testCreateViewRecord() { + // mock 数据 + Long userId = randomLongId(); + Long sourceLibraryId = randomLongId(); + Long targetLibraryId = randomLongId(); + Long documentId = randomLongId(); + Integer type = PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(); + + // 调用 + viewRecordService.createViewRecord(sourceLibraryId, type, documentId, userId); + LocalDateTime firstViewTime = CollUtil.getFirst(viewRecordMapper.selectList()).getCreateTime(); + viewRecordService.createViewRecord(targetLibraryId, type, documentId, userId); + + // 断言 + assertEquals(1, viewRecordMapper.selectList().size()); + PmsKnowledgeViewRecordDO record = CollUtil.getFirst(viewRecordMapper.selectList()); + assertEquals(targetLibraryId, record.getLibraryId()); + assertEquals(type, record.getType()); + assertEquals(documentId, record.getEntityId()); + assertEquals(userId, record.getUserId()); + assertFalse(record.getCreateTime().isBefore(firstViewTime)); + } + + @Test + public void testGetRecentList_groupsAndDeduplicates() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + Long folderId = randomLongId(); + LocalDateTime todayTime = LocalDateTime.now(); + LocalDateTime yesterdayTime = LocalDateTime.now().toLocalDate().atStartOfDay().minusHours(1); + viewRecordMapper.insert(randomViewRecordDO(userId, libraryId, + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), folderId, todayTime)); + viewRecordMapper.insert(randomViewRecordDO(userId, libraryId, + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), randomLongId(), yesterdayTime)); + when(interactionTargetService.getReadableItemList(anyCollection(), eq(userId))).thenReturn(Arrays.asList( + new PmsKnowledgeInteractionItemRespVO().setId(randomLongId()) + .setType(PmsKnowledgeObjectTypeEnum.FOLDER.getType()).setEntityId(folderId) + .setCreateTime(todayTime), + new PmsKnowledgeInteractionItemRespVO().setId(randomLongId()) + .setType(PmsKnowledgeObjectTypeEnum.DOCUMENT.getType()).setEntityId(randomLongId()) + .setCreateTime(yesterdayTime))); + + // 调用 + PmsKnowledgeRecentListRespVO recentItems = viewRecordService.getRecentViewRecordList(null, userId); + + // 断言 + assertEquals(1, recentItems.getTodayItems().size()); + assertEquals(folderId, CollUtil.getFirst(recentItems.getTodayItems()).getEntityId()); + assertEquals(1, recentItems.getYesterdayItems().size()); + assertTrue(CollUtil.isEmpty(recentItems.getRecent30DayItems())); + } + + @Test + public void testDeleteViewRecordsByEntityIds() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + Long folderId = randomLongId(); + Long documentId = randomLongId(); + viewRecordMapper.insert(randomViewRecordDO(userId, libraryId, + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), folderId, LocalDateTime.now())); + viewRecordMapper.insert(randomViewRecordDO(userId, libraryId, + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), documentId, LocalDateTime.now())); + + // 调用 + viewRecordService.deleteViewRecordsByEntityIds(Collections.singleton(folderId), + Collections.singleton(documentId)); + + // 断言 + assertTrue(CollUtil.isEmpty(viewRecordMapper.selectListByUserIdAndCreateTimeAfter( + userId, LocalDateTime.now().minusDays(1)))); + } + + @Test + public void testUpdateViewRecordLibraryIdByEntityIds() { + // mock 数据 + Long userId = randomLongId(); + Long sourceLibraryId = randomLongId(); + Long targetLibraryId = randomLongId(); + Long entityId = randomLongId(); + PmsKnowledgeViewRecordDO folderRecord = randomViewRecordDO(userId, sourceLibraryId, + PmsKnowledgeObjectTypeEnum.FOLDER.getType(), entityId, LocalDateTime.now()); + viewRecordMapper.insert(folderRecord); + PmsKnowledgeViewRecordDO fileRecord = randomViewRecordDO(userId, sourceLibraryId, + PmsKnowledgeObjectTypeEnum.FILE.getType(), entityId, LocalDateTime.now()); + viewRecordMapper.insert(fileRecord); + PmsKnowledgeViewRecordDO otherRecord = randomViewRecordDO(userId, sourceLibraryId, + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), randomLongId(), LocalDateTime.now()); + viewRecordMapper.insert(otherRecord); + + // 调用 + viewRecordService.updateViewRecordLibraryIdByEntityIds(Collections.singleton(entityId), + Collections.singleton(entityId), targetLibraryId); + + // 断言 + assertEquals(targetLibraryId, viewRecordMapper.selectById(folderRecord.getId()).getLibraryId()); + assertEquals(targetLibraryId, viewRecordMapper.selectById(fileRecord.getId()).getLibraryId()); + assertEquals(sourceLibraryId, viewRecordMapper.selectById(otherRecord.getId()).getLibraryId()); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeViewRecordDO randomViewRecordDO(Long userId, Long libraryId, Integer type, + Long entityId, LocalDateTime createTime) { + return randomPojo(PmsKnowledgeViewRecordDO.class, record -> record.setId(null).setLibraryId(libraryId) + .setType(type).setEntityId(entityId).setUserId(userId).setCreateTime(createTime)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupServiceImplTest.java new file mode 100644 index 000000000..80f48a64e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeGroupServiceImplTest.java @@ -0,0 +1,203 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeGroupSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.group.PmsKnowledgeLibraryMoveGroupReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeGroupDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeGroupRelationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeGroupMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeGroupRelationMapper; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeGroupTypeEnum; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_GROUP_DEFAULT_CANNOT_DELETE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeGroupServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeGroupServiceImpl.class) +public class PmsKnowledgeGroupServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeGroupServiceImpl groupService; + + @Resource + private PmsKnowledgeGroupMapper groupMapper; + @Resource + private PmsKnowledgeGroupRelationMapper relationMapper; + + @MockitoBean + private PmsKnowledgeLibraryMemberService libraryMemberService; + + @Test + public void testCreateKnowledgeGroup_success() { + // 准备参数 + Long userId = randomLongId(); + PmsKnowledgeGroupSaveReqVO reqVO = new PmsKnowledgeGroupSaveReqVO().setName(" 产品资料 "); + + // 调用 + Long groupId = groupService.createGroup(reqVO, userId); + + // 断言 + PmsKnowledgeGroupDO group = groupMapper.selectById(groupId); + assertEquals("产品资料", group.getName()); + assertEquals(PmsKnowledgeGroupTypeEnum.CUSTOM.getType(), group.getType()); + assertEquals(1, groupMapper.selectListByUserId(userId).size()); + } + + @Test + public void testGetKnowledgeGroupList_defaultGroups() { + // mock 数据 + Long userId = randomLongId(); + groupMapper.insert(randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.CUSTOM.getType())); + + // 调用 + List groups = groupService.getGroupList(userId); + + // 断言 + assertEquals(1, groupMapper.selectListByUserId(userId).stream() + .filter(item -> PmsKnowledgeGroupTypeEnum.ALL.getType().equals(item.getType())).count()); + assertEquals(1, groupMapper.selectListByUserId(userId).stream() + .filter(item -> PmsKnowledgeGroupTypeEnum.UNGROUPED.getType().equals(item.getType())).count()); + assertEquals(1, groups.stream() + .filter(item -> PmsKnowledgeGroupTypeEnum.ALL.getType().equals(item.getType())).count()); + assertEquals(1, groups.stream() + .filter(item -> PmsKnowledgeGroupTypeEnum.UNGROUPED.getType().equals(item.getType())).count()); + assertEquals(3, groups.size()); + } + + @Test + public void testMoveKnowledgeGroup_success() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + PmsKnowledgeGroupDO customGroup = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.CUSTOM.getType()); + groupMapper.insert(customGroup); + when(libraryMemberService.validateLibraryReadable(libraryId, userId)) + .thenReturn(new PmsKnowledgeLibraryDO().setId(libraryId)); + // 准备参数 + PmsKnowledgeLibraryMoveGroupReqVO reqVO = new PmsKnowledgeLibraryMoveGroupReqVO() + .setLibraryId(libraryId).setGroupId(customGroup.getId()); + + // 调用 + groupService.moveLibraryToGroup(reqVO, userId); + + // 断言 + PmsKnowledgeGroupRelationDO relation = relationMapper.selectByUserIdAndLibraryId(userId, libraryId); + assertNotNull(relation); + assertEquals(customGroup.getId(), relation.getGroupId()); + } + + @Test + public void testGetGroupLibraryCountMap_success() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId1 = randomLongId(); + Long libraryId2 = randomLongId(); + Long libraryId3 = randomLongId(); + PmsKnowledgeGroupDO allGroup = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.ALL.getType()); + PmsKnowledgeGroupDO ungroupedGroup = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.UNGROUPED.getType()); + PmsKnowledgeGroupDO customGroup = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.CUSTOM.getType()); + groupMapper.insert(allGroup); + groupMapper.insert(ungroupedGroup); + groupMapper.insert(customGroup); + relationMapper.insert(new PmsKnowledgeGroupRelationDO().setUserId(userId).setGroupId(customGroup.getId()) + .setLibraryId(libraryId1).setSort(0)); + relationMapper.insert(new PmsKnowledgeGroupRelationDO().setUserId(userId).setGroupId(customGroup.getId()) + .setLibraryId(libraryId1).setSort(1)); + when(libraryMemberService.getReadableLibraryIdList(userId)) + .thenReturn(Arrays.asList(libraryId1, libraryId2, libraryId3)); + + // 调用 + Map countMap = groupService.getGroupLibraryCountMap(userId, + Arrays.asList(allGroup.getId(), ungroupedGroup.getId(), customGroup.getId())); + + // 断言:重复关系按知识库去重,未分组数量不被重复关系影响 + assertEquals(3, countMap.get(allGroup.getId())); + assertEquals(2, countMap.get(ungroupedGroup.getId())); + assertEquals(1, countMap.get(customGroup.getId())); + } + + @Test + public void testMoveKnowledgeGroup_ungrouped() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + PmsKnowledgeGroupDO customGroup = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.CUSTOM.getType()); + groupMapper.insert(customGroup); + PmsKnowledgeGroupDO ungrouped = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.UNGROUPED.getType()); + groupMapper.insert(ungrouped); + relationMapper.insert(new PmsKnowledgeGroupRelationDO().setUserId(userId).setGroupId(customGroup.getId()) + .setLibraryId(libraryId).setSort(0)); + when(libraryMemberService.validateLibraryReadable(libraryId, userId)) + .thenReturn(new PmsKnowledgeLibraryDO().setId(libraryId)); + // 准备参数 + PmsKnowledgeLibraryMoveGroupReqVO reqVO = new PmsKnowledgeLibraryMoveGroupReqVO() + .setLibraryId(libraryId).setGroupId(ungrouped.getId()); + + // 调用 + groupService.moveLibraryToGroup(reqVO, userId); + + // 断言 + assertNull(relationMapper.selectByUserIdAndLibraryId(userId, libraryId)); + } + + @Test + public void testFilterLibraryIdListByGroup_ungrouped() { + // mock 数据 + Long userId = randomLongId(); + Long groupedLibraryId = randomLongId(); + Long ungroupedLibraryId = randomLongId(); + PmsKnowledgeGroupDO customGroup = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.CUSTOM.getType()); + groupMapper.insert(customGroup); + PmsKnowledgeGroupDO ungrouped = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.UNGROUPED.getType()); + groupMapper.insert(ungrouped); + relationMapper.insert(new PmsKnowledgeGroupRelationDO().setUserId(userId).setGroupId(customGroup.getId()) + .setLibraryId(groupedLibraryId).setSort(0)); + + // 调用 + List filteredLibraryIds = groupService.filterLibraryIdListByGroup(ungrouped.getId(), userId, + Arrays.asList(groupedLibraryId, ungroupedLibraryId)); + + // 断言 + assertEquals(Collections.singletonList(ungroupedLibraryId), filteredLibraryIds); + } + + @Test + public void testDeleteKnowledgeGroup_defaultGroup() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeGroupDO allGroup = randomGroupDO(userId, PmsKnowledgeGroupTypeEnum.ALL.getType()); + groupMapper.insert(allGroup); + + // 调用,并断言异常 + assertServiceException(() -> groupService.deleteGroup(allGroup.getId(), userId), + KNOWLEDGE_GROUP_DEFAULT_CANNOT_DELETE); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeGroupDO randomGroupDO(Long userId, Integer type) { + return randomPojo(PmsKnowledgeGroupDO.class, group -> group.setId(null).setUserId(userId) + .setName(PmsKnowledgeGroupTypeEnum.valueOf(type).getName()).setSort(type).setType(type)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberServiceImplTest.java new file mode 100644 index 000000000..ec074e8d6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryMemberServiceImplTest.java @@ -0,0 +1,393 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.member.PmsKnowledgeLibraryUpdateMemberListReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMemberMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_ADMIN_REQUIRED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_CREATOR_CANNOT_REMOVE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_MEMBERS_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeLibraryMemberServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeLibraryMemberServiceImpl.class) +public class PmsKnowledgeLibraryMemberServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeLibraryMemberServiceImpl memberService; + + @Resource + private PmsKnowledgeLibraryMemberMapper memberMapper; + + @MockitoBean + private PmsKnowledgeLibraryService libraryService; + @MockitoBean + private PmsKnowledgeContentPermissionService contentPermissionService; + @MockitoBean + private PermissionApi permissionApi; + @MockitoBean + private AdminUserApi adminUserApi; + @MockitoBean + private DeptApi deptApi; + + @BeforeEach + public void setUpCloudApiMocks() { + lenient().when(permissionApi.hasAnyRoles(anyLong(), org.mockito.ArgumentMatchers.any(String[].class))) + .thenReturn(success(false)); + lenient().when(adminUserApi.getUser(anyLong())).thenReturn(success(null)); + lenient().when(adminUserApi.validateUserList(anyCollection())).thenReturn(success(true)); + lenient().when(deptApi.validateDeptList(anyCollection())).thenReturn(success(true)); + } + + @Test + public void testValidateLibraryReadable_privateDenied() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, false)); + + // 调用,并断言异常 + assertServiceException(() -> memberService.validateLibraryReadable(libraryId, userId), + KNOWLEDGE_LIBRARY_ACCESS_DENIED); + } + + @Test + public void testValidateLibraryReadable_privateMember() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(libraryId, false); + when(libraryService.getLibrary(libraryId)).thenReturn(library); + memberMapper.insert(randomMemberDO(libraryId, userId, null, + PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel())); + + // 调用 + PmsKnowledgeLibraryDO readableLibrary = memberService.validateLibraryReadable(libraryId, userId); + + // 断言 + assertEquals(library, readableLibrary); + } + + @Test + public void testValidateLibraryReadable_privateDeptMember() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + Long deptId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(libraryId, false); + when(libraryService.getLibrary(libraryId)).thenReturn(library); + when(adminUserApi.getUser(userId)).thenReturn(success( + new AdminUserRespDTO().setId(userId).setDeptId(deptId))); + memberMapper.insert(randomMemberDO(libraryId, null, deptId, + PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel())); + + // 调用 + PmsKnowledgeLibraryDO readableLibrary = memberService.validateLibraryReadable(libraryId, userId); + + // 断言 + assertEquals(library, readableLibrary); + } + + @Test + public void testValidateLibraryReadable_parentDeptMemberDenied() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + Long deptId = randomLongId(); + Long parentDeptId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, false)); + when(adminUserApi.getUser(userId)).thenReturn(success( + new AdminUserRespDTO().setId(userId).setDeptId(deptId))); + memberMapper.insert(randomMemberDO(libraryId, null, parentDeptId, + PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel())); + + // 调用,并断言异常 + assertServiceException(() -> memberService.validateLibraryReadable(libraryId, userId), + KNOWLEDGE_LIBRARY_ACCESS_DENIED); + } + + @Test + public void testGetJoinedLibraryIdList_filterInvalidLibrary() { + // mock 数据 + Long userId = randomLongId(); + Long invalidLibraryId = randomLongId(); + Long validLibraryId = randomLongId(); + memberMapper.insert(randomMemberDO(invalidLibraryId, userId, null, + PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel())); + memberMapper.insert(randomMemberDO(validLibraryId, userId, null, + PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel())); + when(libraryService.getLibraryIdList(null)).thenReturn(Collections.singletonList(validLibraryId)); + + // 调用 + List joinedLibraryIds = memberService.getJoinedLibraryIdList(userId); + + // 断言 + assertEquals(Collections.singletonList(validLibraryId), joinedLibraryIds); + } + + @Test + public void testCreateMemberList_success() { + // 准备参数 + Long libraryId = randomLongId(); + Long creatorUserId = randomLongId(); + Long adminUserId = randomLongId(); + Long memberUserId = randomLongId(); + // 调用 + memberService.createLibraryMemberList(libraryId, creatorUserId, + Collections.singletonList(adminUserId), Collections.singletonList(memberUserId)); + + // 断言 + List members = memberMapper.selectListByLibraryId(libraryId); + assertEquals(3, members.size()); + assertNotNull(CollUtil.findOne(members, member -> + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel().equals(member.getLevel()))); + assertNotNull(CollUtil.findOne(members, member -> + adminUserId.equals(member.getUserId()) + && PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel().equals(member.getLevel()))); + assertNotNull(CollUtil.findOne(members, member -> + memberUserId.equals(member.getUserId()) + && PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel().equals(member.getLevel()))); + verify(adminUserApi).validateUserList(new LinkedHashSet<>(Arrays.asList(adminUserId, memberUserId))); + } + + @Test + public void testCreateMemberList_duplicateMember_invalid() { + // 准备参数 + Long libraryId = randomLongId(); + Long creatorUserId = randomLongId(); + Long memberUserId = randomLongId(); + // 调用,并断言异常 + assertServiceException(() -> memberService.createLibraryMemberList(libraryId, creatorUserId, + Arrays.asList(memberUserId, memberUserId), Collections.emptyList()), + KNOWLEDGE_LIBRARY_MEMBERS_INVALID); + } + + @Test + public void testCreateMemberList_duplicateRoleMember_invalid() { + // 准备参数 + Long libraryId = randomLongId(); + Long creatorUserId = randomLongId(); + Long memberUserId = randomLongId(); + + // 调用,并断言异常 + assertServiceException(() -> memberService.createLibraryMemberList(libraryId, creatorUserId, + Collections.singletonList(memberUserId), Collections.singletonList(memberUserId)), + KNOWLEDGE_LIBRARY_MEMBERS_INVALID); + } + + @Test + public void testIsLibraryAdmin_publicAdminMemberAllowed() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, true)); + memberMapper.insert(randomMemberDO(libraryId, userId, null, + PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel())); + + // 调用,并断言:公开库管理员可以参与库和内容协作 + assertTrue(memberService.isLibraryAdmin(libraryId, userId)); + } + + @Test + public void testValidateLibraryAdmin_publicAdminMemberAllowed() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, true)); + memberMapper.insert(randomMemberDO(libraryId, userId, null, + PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel())); + + // 调用 + assertNotNull(memberService.validateLibraryAdmin(libraryId, userId)); + } + + @Test + public void testValidateLibraryCreator_adminMemberDenied() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, true)); + memberMapper.insert(randomMemberDO(libraryId, userId, null, + PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel())); + + // 调用,并断言:管理员可以协作,但不能执行创建人级别的高危操作 + assertServiceException(() -> memberService.validateLibraryCreator(libraryId, userId), + KNOWLEDGE_LIBRARY_ADMIN_REQUIRED); + } + + @Test + public void testUpdateMemberList_publicAllowsMembers() { + // mock 数据 + Long libraryId = randomLongId(); + Long creatorUserId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, true)); + memberMapper.insert(randomMemberDO(libraryId, creatorUserId, null, + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + PmsKnowledgeLibraryUpdateMemberListReqVO reqVO = new PmsKnowledgeLibraryUpdateMemberListReqVO() + .setLibraryId(libraryId).setMembers(Collections.singletonList( + new PmsKnowledgeLibraryUpdateMemberListReqVO.Member().setUserId(randomLongId()) + .setLevel(PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel()))); + + // 调用 + memberService.updateLibraryMemberList(reqVO, creatorUserId); + + // 断言 + List members = memberMapper.selectListByLibraryId(libraryId); + assertEquals(2, members.size()); + } + + @Test + public void testUpdateMemberList_deptMember() { + // mock 数据 + Long creatorUserId = randomLongId(); + Long libraryId = randomLongId(); + Long deptId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, false)); + memberMapper.insert(randomMemberDO(libraryId, creatorUserId, null, + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + // 准备参数 + PmsKnowledgeLibraryUpdateMemberListReqVO reqVO = new PmsKnowledgeLibraryUpdateMemberListReqVO() + .setLibraryId(libraryId).setMembers(Collections.singletonList( + new PmsKnowledgeLibraryUpdateMemberListReqVO.Member().setDeptId(deptId) + .setLevel(PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel()))); + + // 调用 + memberService.updateLibraryMemberList(reqVO, creatorUserId); + + // 断言 + List members = memberMapper.selectListByLibraryId(libraryId); + assertEquals(2, members.size()); + assertTrue(members.stream().anyMatch(member -> deptId.equals(member.getDeptId()))); + } + + @Test + public void testUpdateMemberList_retainCreator() { + // mock 数据 + Long creatorUserId = randomLongId(); + Long libraryId = randomLongId(); + Long memberUserId = randomLongId(); + Long oldMemberUserId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, false)); + memberMapper.insert(randomMemberDO(libraryId, creatorUserId, null, + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + memberMapper.insert(randomMemberDO(libraryId, oldMemberUserId, null, + PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel())); + // 准备参数 + PmsKnowledgeLibraryUpdateMemberListReqVO reqVO = new PmsKnowledgeLibraryUpdateMemberListReqVO() + .setLibraryId(libraryId).setMembers(Collections.singletonList( + new PmsKnowledgeLibraryUpdateMemberListReqVO.Member().setUserId(memberUserId) + .setLevel(PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel()))); + + // 调用 + memberService.updateLibraryMemberList(reqVO, creatorUserId); + + // 断言 + List members = memberMapper.selectListByLibraryId(libraryId); + assertEquals(2, members.size()); + assertTrue(members.stream().anyMatch(item -> item.getUserId().equals(creatorUserId))); + assertTrue(members.stream().anyMatch(item -> item.getUserId().equals(memberUserId))); + assertFalse(members.stream().anyMatch(item -> item.getUserId().equals(oldMemberUserId))); + } + + @Test + public void testUpdateMemberList_creatorDuplicated() { + // mock 数据 + Long creatorUserId = randomLongId(); + Long libraryId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, false)); + memberMapper.insert(randomMemberDO(libraryId, creatorUserId, null, + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + // 准备参数 + PmsKnowledgeLibraryUpdateMemberListReqVO reqVO = new PmsKnowledgeLibraryUpdateMemberListReqVO() + .setLibraryId(libraryId).setMembers(Collections.singletonList( + new PmsKnowledgeLibraryUpdateMemberListReqVO.Member().setUserId(creatorUserId) + .setLevel(PmsKnowledgeLibraryMemberLevelEnum.ADMIN.getLevel()))); + + // 调用,并断言异常 + assertServiceException(() -> memberService.updateLibraryMemberList(reqVO, creatorUserId), + KNOWLEDGE_LIBRARY_MEMBERS_INVALID); + } + + @Test + public void testExitLibrary_success() { + // mock 数据 + Long userId = randomLongId(); + Long libraryId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, false)); + memberMapper.insert(randomMemberDO(libraryId, userId, null, + PmsKnowledgeLibraryMemberLevelEnum.MEMBER.getLevel())); + + // 调用 + memberService.exitLibrary(libraryId, userId); + + // 断言 + assertNull(memberMapper.selectByLibraryIdAndUserId(libraryId, userId)); + verify(contentPermissionService).deleteContentPermissionMembersByLibraryId(libraryId, Collections.singleton(userId), + Collections.emptySet()); + } + + @Test + public void testExitLibrary_creatorDenied() { + // mock 数据 + Long creatorUserId = randomLongId(); + Long libraryId = randomLongId(); + when(libraryService.getLibrary(libraryId)).thenReturn(randomLibraryDO(libraryId, false)); + memberMapper.insert(randomMemberDO(libraryId, creatorUserId, null, + PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + + // 调用,并断言异常 + assertServiceException(() -> memberService.exitLibrary(libraryId, creatorUserId), + KNOWLEDGE_LIBRARY_CREATOR_CANNOT_REMOVE); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeLibraryMemberDO randomMemberDO(Long libraryId, Long userId, Long deptId, Integer level) { + return randomPojo(PmsKnowledgeLibraryMemberDO.class, member -> member.setId(null).setLibraryId(libraryId) + .setUserId(userId).setDeptId(deptId).setLevel(level)); + } + + private static PmsKnowledgeLibraryDO randomLibraryDO(Long libraryId, boolean openStatus) { + return randomPojo(PmsKnowledgeLibraryDO.class, library -> library.setId(libraryId) + .setOpenStatus(openStatus).setStatus(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryServiceImplTest.java new file mode 100644 index 000000000..ca82affbf --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryServiceImplTest.java @@ -0,0 +1,215 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibraryPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.library.PmsKnowledgeLibrarySaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMapper; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentStatusEnum; +import cn.iocoder.yudao.module.pms.service.kb.recycle.PmsKnowledgeRecycleService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeLibraryServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeLibraryServiceImpl.class) +public class PmsKnowledgeLibraryServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeLibraryServiceImpl libraryService; + + @Resource + private PmsKnowledgeLibraryMapper libraryMapper; + + @MockitoBean + private PmsKnowledgeLibraryMemberService memberService; + @MockitoBean + private PmsKnowledgeGroupService knowledgeGroupService; + @MockitoBean + private PmsKnowledgeRecycleService recycleService; + @MockitoBean + private PmsKnowledgeLibraryTemplateService libraryTemplateService; + @MockitoBean + private PermissionApi permissionApi; + + @BeforeEach + public void setUpCloudApiMocks() { + lenient().when(permissionApi.hasAnyRoles(anyLong(), any(String[].class))).thenReturn(success(false)); + } + + @Test + public void testCreateLibrary_success() { + // 准备参数 + Long userId = randomLongId(); + Long memberUserId = randomLongId(); + PmsKnowledgeLibrarySaveReqVO reqVO = randomSaveReqVO() + .setAdminUserIds(Collections.singletonList(memberUserId)); + + // 调用 + Long libraryId = libraryService.createLibrary(reqVO, userId); + + // 断言 + PmsKnowledgeLibraryDO library = libraryMapper.selectById(libraryId); + assertEquals(reqVO.getName(), library.getName()); + assertNull(library.getCreator()); // 单元测试无登录上下文,验证 Service 未手工写入审计字段 + verify(memberService).createLibraryMemberList(libraryId, userId, + Collections.singletonList(memberUserId), Collections.emptyList()); + } + + @Test + public void testCreateLibrary_withTemplate() { + // 准备参数 + Long userId = randomLongId(); + PmsKnowledgeLibrarySaveReqVO reqVO = randomSaveReqVO().setTemplateId(5L); + + // 调用 + Long libraryId = libraryService.createLibrary(reqVO, userId); + + // 断言 + verify(memberService).createLibraryMemberList(libraryId, userId, + Collections.emptyList(), Collections.emptyList()); + verify(libraryTemplateService).createTemplateDocumentList(5L, libraryId, userId); + } + + @Test + public void testCreateLibrary_publicKeepsInitialMembers() { + // 准备参数 + Long userId = randomLongId(); + PmsKnowledgeLibrarySaveReqVO reqVO = randomSaveReqVO().setOpenStatus(true) + .setAdminUserIds(Collections.singletonList(randomLongId())) + .setMemberUserIds(Collections.singletonList(randomLongId())); + + // 调用 + libraryService.createLibrary(reqVO, userId); + + // 断言:公开状态只影响可见范围,初始成员仍可用于内容协作 + verify(memberService).createLibraryMemberList(org.mockito.ArgumentMatchers.anyLong(), + org.mockito.ArgumentMatchers.eq(userId), + org.mockito.ArgumentMatchers.eq(reqVO.getAdminUserIds()), + org.mockito.ArgumentMatchers.eq(reqVO.getMemberUserIds())); + } + + @Test + public void testUpdateLibrary_publicKeepsMembers() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + when(memberService.validateLibraryAdmin(libraryId, userId)).thenReturn(randomLibraryDO(true).setId(libraryId)); + PmsKnowledgeLibrarySaveReqVO reqVO = randomSaveReqVO().setId(libraryId).setOpenStatus(true); + + // 调用 + libraryService.updateLibrary(reqVO, userId); + + // 断言:公开状态更新不清理已有成员 + verify(memberService, org.mockito.Mockito.never()).validateLibraryCreator(libraryId, userId); + } + + @Test + public void testUpdateLibrary_privateAdminCanMakePublic() { + // mock 数据 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + when(memberService.validateLibraryAdmin(libraryId, userId)).thenReturn(randomLibraryDO(false).setId(libraryId)); + PmsKnowledgeLibrarySaveReqVO reqVO = randomSaveReqVO().setId(libraryId).setOpenStatus(true); + + // 调用 + libraryService.updateLibrary(reqVO, userId); + + // 断言:私有库管理员可以将知识库公开 + verify(memberService, org.mockito.Mockito.never()).validateLibraryCreator(libraryId, userId); + } + + @Test + public void testGetLibraryPage_accessScope() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO publicLibrary = randomLibraryDO(true); + libraryMapper.insert(publicLibrary); + PmsKnowledgeLibraryDO memberLibrary = randomLibraryDO(false); + libraryMapper.insert(memberLibrary); + libraryMapper.insert(randomLibraryDO(false)); + when(memberService.getJoinedLibraryIdList(userId)) + .thenReturn(Collections.singletonList(memberLibrary.getId())); + // 准备参数 + PmsKnowledgeLibraryPageReqVO reqVO = new PmsKnowledgeLibraryPageReqVO(); + reqVO.setPageNo(1).setPageSize(10); + + // 调用 + PageResult pageResult = libraryService.getLibraryPage(reqVO, userId); + + // 断言 + assertEquals(2, pageResult.getTotal()); + assertTrue(pageResult.getList().stream().anyMatch(item -> item.getId().equals(publicLibrary.getId()))); + assertTrue(pageResult.getList().stream().anyMatch(item -> item.getId().equals(memberLibrary.getId()))); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetLibraryPage_allGroupUsesReadableCandidates() { + // mock 数据 + Long userId = randomLongId(); + Long groupId = randomLongId(); + PmsKnowledgeLibraryDO publicLibrary = randomLibraryDO(true); + libraryMapper.insert(publicLibrary); + PmsKnowledgeLibraryDO memberLibrary = randomLibraryDO(false); + libraryMapper.insert(memberLibrary); + when(memberService.getJoinedLibraryIdList(userId)).thenReturn(Collections.singletonList(memberLibrary.getId())); + List readableLibraryIds = Arrays.asList(publicLibrary.getId(), memberLibrary.getId()); + when(memberService.getReadableLibraryIdList(userId)).thenReturn(readableLibraryIds); + doAnswer(invocation -> new java.util.ArrayList<>((java.util.Collection) invocation.getArgument(2))) + .when(knowledgeGroupService).filterLibraryIdListByGroup(groupId, userId, readableLibraryIds); + + // 调用 + PmsKnowledgeLibraryPageReqVO reqVO = new PmsKnowledgeLibraryPageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + reqVO.setGroupId(groupId); + PageResult pageResult = libraryService.getLibraryPage(reqVO, userId); + + // 断言:默认“全部”分组的候选集必须包含公开库 + assertEquals(2, pageResult.getTotal()); + assertTrue(pageResult.getList().stream().anyMatch(item -> item.getId().equals(publicLibrary.getId()))); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeLibraryDO randomLibraryDO(boolean openStatus) { + return randomPojo(PmsKnowledgeLibraryDO.class, library -> library.setId(null).setName("测试知识库") + .setDescription("测试").setOpenStatus(openStatus).setCoverUrl(null) + .setStatus(PmsKnowledgeDocumentStatusEnum.NORMAL.getStatus()) + .setDeleteUserId(null).setDeleteTime(null)); + } + + private static PmsKnowledgeLibrarySaveReqVO randomSaveReqVO() { + return randomPojo(PmsKnowledgeLibrarySaveReqVO.class, reqVO -> reqVO.setId(null) + .setName("产品知识库").setDescription("产品文档").setOpenStatus(false) + .setCoverUrl(null).setAdminUserIds(Collections.emptyList()) + .setMemberUserIds(Collections.emptyList())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateServiceImplTest.java new file mode 100644 index 000000000..ea45497d0 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/library/PmsKnowledgeLibraryTemplateServiceImplTest.java @@ -0,0 +1,181 @@ +package cn.iocoder.yudao.module.pms.service.kb.library; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.kb.content.vo.document.PmsKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.kb.library.vo.template.PmsKnowledgeLibraryTemplateSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryTemplateDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryTemplateMapper; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_TEMPLATE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_TEMPLATE_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_LIBRARY_TEMPLATE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeLibraryTemplateServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsKnowledgeLibraryTemplateServiceImpl.class) +public class PmsKnowledgeLibraryTemplateServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeLibraryTemplateServiceImpl templateService; + + @Resource + private PmsKnowledgeLibraryTemplateMapper templateMapper; + + @MockitoBean + private PmsKnowledgeDocumentService documentService; + + @org.junit.jupiter.api.BeforeEach + public void setUp() { + if (templateMapper.selectCount(null) > 0) { + return; + } + for (int i = 1; i <= 8; i++) { + List documents = new ArrayList<>(); + int documentCount = i == 1 ? 5 : 1; + for (int j = 1; j <= documentCount; j++) { + documents.add(new PmsKnowledgeLibraryTemplateDO.Document() + .setTitle(i == 1 && j == 1 ? "产品需求文档" + : i == 7 && j == 1 ? "2020.01.01 更新日志" : "模板文档" + j) + .setContent(i == 1 && j == 1 ? "

基本信息

" + : i == 7 && j == 1 ? "

2020.01.01 更新日志

" : "

模板内容

")); + } + templateMapper.insert(new PmsKnowledgeLibraryTemplateDO().setId((long) i) + .setName(i == 1 ? "产品研发" : "模板" + i).setDescription("模板简介") + .setStatus(0).setSort(i * 10).setDocuments(documents)); + } + } + + @Test + public void testGetTemplateList_success() { + // 调用 + List templates = templateService + .getLibraryTemplateList(CommonStatusEnum.ENABLE.getStatus()); + + // 断言 + PmsKnowledgeLibraryTemplateDO template = CollUtil.getFirst(templates); + assertEquals("产品研发", template.getName()); + assertEquals(5, template.getDocuments().size()); + PmsKnowledgeLibraryTemplateDO.Document document = CollUtil.getFirst(template.getDocuments()); + assertEquals("产品需求文档", document.getTitle()); + assertTrue(document.getContent().contains("基本信息")); + } + + @Test + public void testGetTemplateList_withStatus() { + // 准备数据 + templateMapper.insert(new PmsKnowledgeLibraryTemplateDO().setId(99L).setName("关闭模板") + .setStatus(CommonStatusEnum.DISABLE.getStatus()).setSort(99) + .setDocuments(Collections.singletonList(new PmsKnowledgeLibraryTemplateDO.Document() + .setTitle("关闭文档").setContent("

关闭内容

")))); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), templateMapper.selectById(99L).getStatus()); + + // 调用,并断言状态过滤 + List enabledTemplates = templateService + .getLibraryTemplateList(CommonStatusEnum.ENABLE.getStatus()); + List allTemplates = templateService.getLibraryTemplateList(null); + assertTrue(enabledTemplates.stream().noneMatch(template -> template.getId().equals(99L))); + assertTrue(allTemplates.stream().anyMatch(template -> template.getId().equals(99L))); + } + + @Test + public void testCreateLibraryTemplate_success() { + // 准备参数 + PmsKnowledgeLibraryTemplateSaveReqVO saveReqVO = new PmsKnowledgeLibraryTemplateSaveReqVO() + .setName("新模板").setDescription("模板简介").setStatus(0).setSort(100) + .setDocuments(Collections.singletonList(new PmsKnowledgeLibraryTemplateSaveReqVO.Document() + .setTitle("模板文档").setContent("

模板内容

"))); + + // 调用 + Long templateId = templateService.createLibraryTemplate(saveReqVO); + + // 断言 + PmsKnowledgeLibraryTemplateDO template = templateService.getLibraryTemplate(templateId); + assertEquals("新模板", template.getName()); + assertEquals("模板文档", CollUtil.getFirst(template.getDocuments()).getTitle()); + } + + @Test + public void testCreateLibraryTemplate_duplicateName() { + // 准备参数 + PmsKnowledgeLibraryTemplateSaveReqVO saveReqVO = new PmsKnowledgeLibraryTemplateSaveReqVO() + .setName("产品研发").setStatus(0).setSort(100) + .setDocuments(Collections.singletonList(new PmsKnowledgeLibraryTemplateSaveReqVO.Document() + .setTitle("模板文档").setContent("

模板内容

"))); + + // 调用,并断言异常 + assertServiceException(() -> templateService.createLibraryTemplate(saveReqVO), + KNOWLEDGE_LIBRARY_TEMPLATE_NAME_DUPLICATE); + } + + @Test + public void testCreateLibraryTemplate_invalidDocuments() { + // 准备参数 + PmsKnowledgeLibraryTemplateSaveReqVO saveReqVO = new PmsKnowledgeLibraryTemplateSaveReqVO() + .setName("无效模板").setStatus(0).setSort(100).setDocuments(Arrays.asList( + new PmsKnowledgeLibraryTemplateSaveReqVO.Document().setTitle("重复").setContent("内容"), + new PmsKnowledgeLibraryTemplateSaveReqVO.Document().setTitle("重复").setContent("内容"))); + + // 调用,并断言异常 + assertServiceException(() -> templateService.createLibraryTemplate(saveReqVO), + KNOWLEDGE_LIBRARY_TEMPLATE_INVALID); + } + + @Test + public void testCreateTemplateDocumentList_success() { + // 准备参数 + Long libraryId = randomLongId(); + Long userId = randomLongId(); + Long documentId = randomLongId(); + when(documentService.createDocument(any(), eq(userId))).thenReturn(documentId); + + // 调用 + templateService.createTemplateDocumentList(7L, libraryId, userId); + + // 断言 + ArgumentCaptor createCaptor = + ArgumentCaptor.forClass(PmsKnowledgeDocumentCreateReqVO.class); + verify(documentService).createDocument(createCaptor.capture(), eq(userId)); + PmsKnowledgeDocumentCreateReqVO document = createCaptor.getValue(); + assertEquals(libraryId, document.getLibraryId()); + assertEquals(0L, document.getFolderId()); + assertEquals(0L, document.getParentId()); + assertEquals("2020.01.01 更新日志", document.getTitle()); + assertTrue(document.getContent().contains("2020.01.01 更新日志")); + verify(documentService, times(0)).updateDocument(any(), eq(userId)); + } + + @Test + public void testCreateTemplateDocumentList_notExists() { + // 调用,并断言异常 + assertServiceException(() -> templateService.createTemplateDocumentList(99L, randomLongId(), randomLongId()), + KNOWLEDGE_LIBRARY_TEMPLATE_NOT_EXISTS); + verify(documentService, times(0)).createDocument(any(), anyLong()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleServiceImplTest.java new file mode 100644 index 000000000..30de0aecc --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/kb/recycle/PmsKnowledgeRecycleServiceImplTest.java @@ -0,0 +1,385 @@ +package cn.iocoder.yudao.module.pms.service.kb.recycle; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeDocumentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.content.PmsKnowledgeFolderDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.library.PmsKnowledgeLibraryMemberDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.kb.recycle.PmsKnowledgeRecycleRecordDO; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeDocumentMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.content.PmsKnowledgeFolderMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.library.PmsKnowledgeLibraryMemberMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.kb.recycle.PmsKnowledgeRecycleRecordMapper; +import cn.iocoder.yudao.module.pms.enums.kb.PmsKnowledgeObjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeContentLevelEnum; +import cn.iocoder.yudao.module.pms.enums.kb.content.PmsKnowledgeDocumentTypeEnum; +import cn.iocoder.yudao.module.pms.enums.kb.library.PmsKnowledgeLibraryMemberLevelEnum; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeContentPermissionService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentLabelService; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeDocumentServiceImpl; +import cn.iocoder.yudao.module.pms.service.kb.content.PmsKnowledgeFolderServiceImpl; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentCommentService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentLikeService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeDocumentShareService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeFavoriteService; +import cn.iocoder.yudao.module.pms.service.kb.interaction.PmsKnowledgeViewRecordService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeGroupService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryMemberService; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryServiceImpl; +import cn.iocoder.yudao.module.pms.service.kb.library.PmsKnowledgeLibraryTemplateService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.KNOWLEDGE_RECYCLE_TYPE_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * {@link PmsKnowledgeRecycleServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import({PmsKnowledgeRecycleServiceImpl.class, PmsKnowledgeLibraryServiceImpl.class, + PmsKnowledgeFolderServiceImpl.class, PmsKnowledgeDocumentServiceImpl.class}) +public class PmsKnowledgeRecycleServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsKnowledgeRecycleServiceImpl recycleService; + + @Resource + private PmsKnowledgeLibraryMapper libraryMapper; + @Resource + private PmsKnowledgeLibraryMemberMapper memberMapper; + @Resource + private PmsKnowledgeFolderMapper folderMapper; + @Resource + private PmsKnowledgeDocumentMapper documentMapper; + @Resource + private PmsKnowledgeRecycleRecordMapper recycleRecordMapper; + + @MockitoBean + private PmsKnowledgeGroupService knowledgeGroupService; + @MockitoBean + private PmsKnowledgeFavoriteService favoriteService; + @MockitoBean + private PmsKnowledgeDocumentLikeService documentLikeService; + @MockitoBean + private PmsKnowledgeViewRecordService viewRecordService; + @MockitoBean + private PmsKnowledgeDocumentShareService documentShareService; + @MockitoBean + private PmsKnowledgeDocumentCommentService documentCommentService; + @MockitoBean + private PmsKnowledgeContentPermissionService contentPermissionService; + @MockitoBean + private PmsKnowledgeDocumentLabelService documentLabelService; + @MockitoBean + private PermissionApi permissionApi; + @MockitoBean + private AdminUserApi adminUserApi; + @MockitoBean + private PmsKnowledgeLibraryTemplateService libraryTemplateService; + @MockitoBean + private PmsKnowledgeLibraryMemberService memberService; + + @BeforeEach + public void setUpCloudApiMocks() { + lenient().when(permissionApi.hasAnyRoles(anyLong(), any(String[].class))).thenReturn(success(false)); + lenient().when(adminUserApi.getUser(anyLong())).thenReturn(success(null)); + } + + @Test + public void testRestoreDocument_preservesExplicitlyDeletedChild() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeLibraryMemberDO member = randomMemberDO(library.getId(), userId); + memberMapper.insert(member); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.getMemberByLibraryIdAndUserId(library.getId(), userId)).thenReturn(member); + PmsKnowledgeDocumentDO parent = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(parent); + PmsKnowledgeDocumentDO child = randomDocumentDO(library.getId(), 0L, parent.getId()); + documentMapper.insert(child); + recycleService.recycleDocument(child, Collections.singletonList(child), userId); + recycleService.recycleDocument(parent, Arrays.asList(parent, child), userId); + PmsKnowledgeRecycleRecordDO parentRecord = recycleRecordMapper.selectByTypeAndEntityId( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), parent.getId()); + + // 调用 + recycleService.restoreContentRecycle(parentRecord.getId(), userId); + + // 断言 + assertEquals(1, documentMapper.selectById(parent.getId()).getStatus()); + assertEquals(-1, documentMapper.selectById(child.getId()).getStatus()); + assertNotNull(recycleRecordMapper.selectByTypeAndEntityId( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), child.getId())); + } + + @Test + public void testRestoreLibrary_preservesExplicitlyDeletedFolder() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeLibraryMemberDO member = randomMemberDO(library.getId(), userId); + memberMapper.insert(member); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.getMemberByLibraryIdAndUserId(library.getId(), userId)).thenReturn(member); + PmsKnowledgeFolderDO deletedFolder = randomFolderDO(library.getId(), 0L); + folderMapper.insert(deletedFolder); + PmsKnowledgeFolderDO normalFolder = randomFolderDO(library.getId(), 0L); + folderMapper.insert(normalFolder); + recycleService.recycleFolder(deletedFolder, Collections.singletonList(deletedFolder), + Collections.emptyList(), userId); + recycleService.recycleLibrary(library, userId); + PmsKnowledgeRecycleRecordDO libraryRecord = recycleRecordMapper.selectByTypeAndEntityId( + PmsKnowledgeObjectTypeEnum.LIBRARY.getType(), library.getId()); + + // 调用 + recycleService.restoreContentRecycle(libraryRecord.getId(), userId); + + // 断言 + assertEquals(1, libraryMapper.selectById(library.getId()).getStatus()); + assertEquals(-1, folderMapper.selectById(deletedFolder.getId()).getStatus()); + assertEquals(1, folderMapper.selectById(normalFolder.getId()).getStatus()); + } + + @Test + public void testPermanentDeleteDocument_recursive() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeLibraryMemberDO member = randomMemberDO(library.getId(), userId); + memberMapper.insert(member); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.getMemberByLibraryIdAndUserId(library.getId(), userId)).thenReturn(member); + PmsKnowledgeDocumentDO parent = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(parent); + PmsKnowledgeDocumentDO child = randomDocumentDO(library.getId(), 0L, parent.getId()); + documentMapper.insert(child); + recycleService.recycleDocument(parent, Arrays.asList(parent, child), userId); + PmsKnowledgeRecycleRecordDO record = recycleRecordMapper.selectByTypeAndEntityId( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), parent.getId()); + + // 调用 + recycleService.deleteContentRecycle(record.getId(), userId); + + // 断言 + assertNull(documentMapper.selectById(parent.getId())); + assertNull(documentMapper.selectById(child.getId())); + assertNull(recycleRecordMapper.selectById(record.getId())); + } + + @Test + public void testRestoreDocument_repeatedRecycle() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeLibraryMemberDO member = randomMemberDO(library.getId(), userId); + memberMapper.insert(member); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.getMemberByLibraryIdAndUserId(library.getId(), userId)).thenReturn(member); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(document); + recycleService.recycleDocument(document, Collections.singletonList(document), userId); + PmsKnowledgeRecycleRecordDO firstRecord = recycleRecordMapper.selectByTypeAndEntityId( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), document.getId()); + recycleService.restoreContentRecycle(firstRecord.getId(), userId); + + // 调用 + document = documentMapper.selectById(document.getId()); + recycleService.recycleDocument(document, Collections.singletonList(document), userId); + PmsKnowledgeRecycleRecordDO secondRecord = recycleRecordMapper.selectByTypeAndEntityId( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), document.getId()); + recycleService.restoreContentRecycle(secondRecord.getId(), userId); + + // 断言 + assertEquals(1, documentMapper.selectById(document.getId()).getStatus()); + assertNull(recycleRecordMapper.selectByTypeAndEntityId( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), document.getId())); + } + + @Test + public void testGetContentRecycleList_filtersByContentPermission() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeLibraryMemberDO member = randomMemberDO(library.getId(), userId); + memberMapper.insert(member); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.getMemberByLibraryIdAndUserId(library.getId(), userId)).thenReturn(member); + PmsKnowledgeDocumentDO editableDocument = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(editableDocument); + PmsKnowledgeDocumentDO previewDocument = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(previewDocument); + recycleService.recycleDocument(editableDocument, Collections.singletonList(editableDocument), userId); + recycleService.recycleDocument(previewDocument, Collections.singletonList(previewDocument), userId); + Map levelMap = new LinkedHashMap<>(); + levelMap.put(editableDocument.getPermissionId(), PmsKnowledgeContentLevelEnum.EDIT.getLevel()); + levelMap.put(previewDocument.getPermissionId(), PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + + // mock 方法 + when(contentPermissionService.getCurrentUserContentPermissionLevelMap(anyCollection(), eq(library.getId()), eq(userId))) + .thenReturn(levelMap); + + // 调用 + List records = recycleService.getContentRecycleList(library.getId(), userId); + + // 断言 + assertEquals(1, records.size()); + assertEquals(editableDocument.getId(), CollUtil.getFirst(records).getEntityId()); + } + + @Test + public void testGetContentRecycleList_sameEntityIdUsesObjectTypePermission() { + // mock 数据:文件夹和文档来自不同表,刻意使用相同的业务 ID + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeLibraryMemberDO member = randomMemberDO(library.getId(), userId); + memberMapper.insert(member); + when(memberService.validateLibraryReadable(library.getId(), userId)).thenReturn(library); + when(memberService.getMemberByLibraryIdAndUserId(library.getId(), userId)).thenReturn(member); + Long sharedEntityId = 100L; + Long editablePermissionId = randomLongId(); + Long previewPermissionId = randomLongId(); + PmsKnowledgeFolderDO folder = randomFolderDO(library.getId(), 0L) + .setId(sharedEntityId).setPermissionId(editablePermissionId); + PmsKnowledgeDocumentDO document = randomDocumentDO(library.getId(), 0L, 0L) + .setId(sharedEntityId).setPermissionId(previewPermissionId); + folderMapper.insert(folder); + documentMapper.insert(document); + recycleService.recycleFolder(folder, Collections.singletonList(folder), Collections.emptyList(), userId); + recycleService.recycleDocument(document, Collections.singletonList(document), userId); + Map levelMap = new LinkedHashMap<>(); + levelMap.put(editablePermissionId, PmsKnowledgeContentLevelEnum.EDIT.getLevel()); + levelMap.put(previewPermissionId, PmsKnowledgeContentLevelEnum.PREVIEW.getLevel()); + + // mock 方法 + when(contentPermissionService.getCurrentUserContentPermissionLevelMap(anyCollection(), eq(library.getId()), eq(userId))) + .thenReturn(levelMap); + + // 调用 + List records = recycleService.getContentRecycleList(library.getId(), userId); + + // 断言:文件夹权限为可删除,文档权限为仅预览,不能因同号 ID 串用权限 + assertEquals(1, records.size()); + assertEquals(PmsKnowledgeObjectTypeEnum.FOLDER.getType(), CollUtil.getFirst(records).getType()); + assertEquals(sharedEntityId, CollUtil.getFirst(records).getEntityId()); + } + + @Test + public void testRestoreContentRecycle_unknownType() { + // mock 数据 + PmsKnowledgeRecycleRecordDO record = randomPojo(PmsKnowledgeRecycleRecordDO.class, + item -> item.setId(null).setLibraryId(randomLongId()).setType(999) + .setEntityId(randomLongId()).setName("未知类型")); + recycleRecordMapper.insert(record); + + // 调用并断言统一业务异常 + assertServiceException(() -> recycleService.restoreContentRecycle(record.getId(), randomLongId()), + KNOWLEDGE_RECYCLE_TYPE_INVALID); + } + + @Test + public void testDeleteContentRecycle_unknownType() { + // mock 数据 + PmsKnowledgeRecycleRecordDO record = randomPojo(PmsKnowledgeRecycleRecordDO.class, + item -> item.setId(null).setLibraryId(randomLongId()).setType(999) + .setEntityId(randomLongId()).setName("未知类型")); + recycleRecordMapper.insert(record); + + // 调用并断言统一业务异常 + assertServiceException(() -> recycleService.deleteContentRecycle(record.getId(), randomLongId()), + KNOWLEDGE_RECYCLE_TYPE_INVALID); + } + + @Test + public void testCleanExpiredRecycleRecords_onlyDeletesExpired() { + // mock 数据 + Long userId = randomLongId(); + PmsKnowledgeLibraryDO library = randomLibraryDO(); + libraryMapper.insert(library); + PmsKnowledgeDocumentDO expiredDocument = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(expiredDocument); + recycleService.recycleDocument(expiredDocument, Collections.singletonList(expiredDocument), userId); + PmsKnowledgeRecycleRecordDO expiredRecord = recycleRecordMapper.selectByTypeAndEntityId( + PmsKnowledgeObjectTypeEnum.DOCUMENT.getType(), expiredDocument.getId()); + recycleRecordMapper.updateById(new PmsKnowledgeRecycleRecordDO().setId(expiredRecord.getId()) + .setDeleteTime(LocalDateTime.now().minusDays(31))); + PmsKnowledgeDocumentDO recentDocument = randomDocumentDO(library.getId(), 0L, 0L); + documentMapper.insert(recentDocument); + recycleService.recycleDocument(recentDocument, Collections.singletonList(recentDocument), userId); + + // 调用 + int cleanCount = recycleService.deleteExpiredRecycleRecords(LocalDateTime.now().minusDays(30)); + + // 断言 + assertEquals(1, cleanCount); + assertNull(documentMapper.selectById(expiredDocument.getId())); + assertNotNull(documentMapper.selectById(recentDocument.getId())); + } + + @Test + public void testDeleteExpiredRecycleRecords_empty() { + // 调用 + int cleanCount = recycleService.deleteExpiredRecycleRecords(LocalDateTime.now().minusDays(30)); + + // 断言 + assertEquals(0, cleanCount); + } + + // ========== 随机对象 ========== + + private PmsKnowledgeLibraryDO randomLibraryDO() { + return randomPojo(PmsKnowledgeLibraryDO.class, library -> library.setId(null).setName("测试知识库") + .setOpenStatus(false).setStatus(1)); + } + + private PmsKnowledgeLibraryMemberDO randomMemberDO(Long libraryId, Long userId) { + return randomPojo(PmsKnowledgeLibraryMemberDO.class, member -> member.setId(null).setLibraryId(libraryId) + .setUserId(userId).setLevel(PmsKnowledgeLibraryMemberLevelEnum.CREATOR.getLevel())); + } + + private PmsKnowledgeFolderDO randomFolderDO(Long libraryId, Long parentId) { + return randomPojo(PmsKnowledgeFolderDO.class, folder -> folder.setId(null).setLibraryId(libraryId) + .setPermissionId(randomLongId()).setParentId(parentId).setTitle("测试目录").setStatus(1)); + } + + private PmsKnowledgeDocumentDO randomDocumentDO(Long libraryId, Long folderId, Long parentId) { + return randomPojo(PmsKnowledgeDocumentDO.class, document -> document.setId(null).setLibraryId(libraryId) + .setPermissionId(randomLongId()).setFolderId(folderId).setParentId(parentId).setTitle("测试文档") + .setType(PmsKnowledgeDocumentTypeEnum.RICH_TEXT.getType()).setStatus(1)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationServiceImplTest.java new file mode 100644 index 000000000..d6fc459b9 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/iteration/PmsIterationServiceImplTest.java @@ -0,0 +1,311 @@ +package cn.iocoder.yudao.module.pms.service.pm.iteration; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationOverviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.iteration.vo.PmsIterationStartReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemActivityDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.iteration.PmsIterationMapper; +import cn.iocoder.yudao.module.pms.enums.pm.iteration.PmsIterationStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemActivityService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemWorkLogService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.ITERATION_STATUS_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsIterationServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsIterationServiceImpl.class) +public class PmsIterationServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsIterationServiceImpl iterationService; + + @Resource + private PmsIterationMapper iterationMapper; + + @MockitoBean + private PmsProjectMemberService projectMemberService; + @MockitoBean + private PmsWorkItemService workItemService; + @MockitoBean + private PmsWorkItemActivityService workItemActivityService; + @MockitoBean + private PmsWorkItemWorkLogService workItemWorkLogService; + + @Test + public void testCreateIteration_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsProjectDO project = randomProjectDO(projectId); + when(projectMemberService.validateProjectWritable(projectId, userId)).thenReturn(project); + iterationMapper.insert(randomIterationDO(projectId, PmsIterationStatusEnum.PLANNED.getStatus()).setSort(3)); + // 准备参数 + PmsIterationSaveReqVO reqVO = randomIterationSaveReqVO(projectId); + + // 调用 + Long iterationId = iterationService.createIteration(reqVO, userId); + + // 断言 + PmsIterationDO iteration = iterationMapper.selectById(iterationId); + assertNotNull(iteration); + assertEquals(reqVO.getName(), iteration.getName()); + assertEquals(PmsIterationStatusEnum.PLANNED.getStatus(), iteration.getStatus()); + assertEquals(4, iteration.getSort()); + verify(projectMemberService).validateProjectMemberExists(projectId, reqVO.getOwnerUserId()); + } + + @Test + public void testUpdateIteration_success() { + // mock 数据 + Long userId = randomLongId(); + PmsIterationDO iteration = randomIterationDO(randomLongId(), PmsIterationStatusEnum.PLANNED.getStatus()); + iterationMapper.insert(iteration); + when(projectMemberService.validateProjectWritable(iteration.getProjectId(), userId)) + .thenReturn(randomProjectDO(iteration.getProjectId())); + // 准备参数 + PmsIterationSaveReqVO reqVO = randomPojo(PmsIterationSaveReqVO.class, + item -> item.setId(iteration.getId()).setProjectId(iteration.getProjectId()) + .setName("第二期迭代").setOwnerUserId(randomLongId()).setTarget("交付第二期") + .setStartTime(LocalDateTime.now()).setEndTime(LocalDateTime.now().plusDays(10))); + + // 调用 + iterationService.updateIteration(reqVO, userId); + + // 断言 + PmsIterationDO updatedIteration = iterationMapper.selectById(iteration.getId()); + assertEquals(reqVO.getName(), updatedIteration.getName()); + assertEquals(reqVO.getTarget(), updatedIteration.getTarget()); + assertEquals(iteration.getStatus(), updatedIteration.getStatus()); + } + + @Test + public void testStartIteration_success() { + // mock 数据 + Long userId = randomLongId(); + PmsIterationDO iteration = randomIterationDO(randomLongId(), PmsIterationStatusEnum.PLANNED.getStatus()); + iterationMapper.insert(iteration); + when(projectMemberService.validateProjectWritable(iteration.getProjectId(), userId)) + .thenReturn(randomProjectDO(iteration.getProjectId())); + // 准备参数 + LocalDateTime startTime = LocalDateTime.now(); + PmsIterationStartReqVO reqVO = new PmsIterationStartReqVO().setId(iteration.getId()) + .setStartTime(startTime).setEndTime(startTime.plusDays(14)); + + // 调用 + iterationService.startIteration(reqVO, userId); + + // 断言 + PmsIterationDO startedIteration = iterationMapper.selectById(iteration.getId()); + assertEquals(PmsIterationStatusEnum.ACTIVE.getStatus(), startedIteration.getStatus()); + assertEquals(reqVO.getStartTime(), startedIteration.getStartTime()); + assertEquals(reqVO.getEndTime(), startedIteration.getEndTime()); + } + + @Test + public void testStartIteration_statusInvalid() { + // mock 数据 + Long userId = randomLongId(); + PmsIterationDO iteration = randomIterationDO(randomLongId(), PmsIterationStatusEnum.COMPLETED.getStatus()); + iterationMapper.insert(iteration); + when(projectMemberService.validateProjectWritable(iteration.getProjectId(), userId)) + .thenReturn(randomProjectDO(iteration.getProjectId())); + // 准备参数 + LocalDateTime startTime = LocalDateTime.now(); + PmsIterationStartReqVO reqVO = new PmsIterationStartReqVO().setId(iteration.getId()) + .setStartTime(startTime).setEndTime(startTime.plusDays(14)); + + // 调用,并断言异常 + assertServiceException(() -> iterationService.startIteration(reqVO, userId), ITERATION_STATUS_INVALID); + } + + @Test + public void testCompleteIteration_success() { + // mock 数据 + Long userId = randomLongId(); + PmsIterationDO iteration = randomIterationDO(randomLongId(), PmsIterationStatusEnum.ACTIVE.getStatus()); + iterationMapper.insert(iteration); + when(projectMemberService.validateProjectWritable(iteration.getProjectId(), userId)) + .thenReturn(randomProjectDO(iteration.getProjectId())); + + // 调用 + iterationService.completeIteration(iteration.getId(), userId); + + // 断言 + PmsIterationDO completedIteration = iterationMapper.selectById(iteration.getId()); + assertEquals(PmsIterationStatusEnum.COMPLETED.getStatus(), completedIteration.getStatus()); + assertNotNull(completedIteration.getFinishTime()); + } + + @Test + public void testDeleteIteration_success() { + // mock 数据 + Long userId = randomLongId(); + PmsIterationDO iteration = randomIterationDO(randomLongId(), PmsIterationStatusEnum.PLANNED.getStatus()); + iterationMapper.insert(iteration); + when(projectMemberService.validateProjectWritable(iteration.getProjectId(), userId)) + .thenReturn(randomProjectDO(iteration.getProjectId())); + + // 调用 + iterationService.deleteIteration(iteration.getId(), userId); + + // 断言 + assertNull(iterationMapper.selectById(iteration.getId())); + verify(workItemService).clearWorkItemIterationId(iteration.getId()); + } + + @Test + public void testGetIterationPage_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsIterationDO plannedIteration = randomIterationDO(projectId, PmsIterationStatusEnum.PLANNED.getStatus()) + .setName("第一期"); + iterationMapper.insert(plannedIteration); + iterationMapper.insert(randomIterationDO(projectId, PmsIterationStatusEnum.COMPLETED.getStatus()) + .setName("历史迭代")); + // 准备参数 + PmsIterationPageReqVO reqVO = new PmsIterationPageReqVO().setProjectId(projectId) + .setStatus(PmsIterationStatusEnum.PLANNED.getStatus()).setName("第一"); + + // 调用 + PageResult pageResult = iterationService.getIterationPage(reqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(plannedIteration.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + verify(projectMemberService).validateProjectReadable(projectId, userId); + } + + @Test + public void testGetProjectIterationStatusCountMap_success() { + // mock 数据 + Long firstProjectId = randomLongId(); + Long secondProjectId = randomLongId(); + iterationMapper.insert(randomIterationDO(firstProjectId, PmsIterationStatusEnum.PLANNED.getStatus())); + iterationMapper.insert(randomIterationDO(firstProjectId, PmsIterationStatusEnum.PLANNED.getStatus())); + iterationMapper.insert(randomIterationDO(firstProjectId, PmsIterationStatusEnum.COMPLETED.getStatus())); + iterationMapper.insert(randomIterationDO(secondProjectId, PmsIterationStatusEnum.ACTIVE.getStatus())); + + // 调用 + Map> countMap = iterationService.getProjectIterationStatusCountMap( + Arrays.asList(firstProjectId, secondProjectId)); + + // 断言 + assertEquals(2L, countMap.get(firstProjectId).get(PmsIterationStatusEnum.PLANNED.getStatus())); + assertEquals(1L, countMap.get(firstProjectId).get(PmsIterationStatusEnum.COMPLETED.getStatus())); + assertEquals(1L, countMap.get(secondProjectId).get(PmsIterationStatusEnum.ACTIVE.getStatus())); + } + + @Test + public void testGetIterationOverview_success() { + // mock 数据 + Long userId = randomLongId(); + LocalDateTime iterationStartTime = LocalDateTime.now().minusDays(2).toLocalDate().atStartOfDay(); + PmsIterationDO iteration = randomIterationDO(randomLongId(), PmsIterationStatusEnum.ACTIVE.getStatus()) + .setStartTime(iterationStartTime).setEndTime(iterationStartTime.plusDays(4)); + iterationMapper.insert(iteration); + PmsWorkItemDO pendingWorkItem = randomWorkItemDO(PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType()).setEstimatedHours(8); + PmsWorkItemDO completedWorkItem = randomWorkItemDO(PmsWorkItemTypeEnum.DEFECT.getType(), + PmsWorkItemStatusTypeEnum.COMPLETED.getType()).setEstimatedHours(4); + when(workItemService.getActiveWorkItemListByIterationId(iteration.getId())) + .thenReturn(Arrays.asList(pendingWorkItem, completedWorkItem)); + PmsWorkItemWorkLogDO workLog = new PmsWorkItemWorkLogDO(); + workLog.setWorkItemId(pendingWorkItem.getId()); + workLog.setActualHours(3); + workLog.setRemainingHours(5); + workLog.setCreateTime(LocalDateTime.now().withHour(10)); + when(workItemWorkLogService.getWorkItemWorkLogListByWorkItemIds(ArgumentMatchers.anyCollection())) + .thenReturn(Collections.singletonList(workLog)); + when(workItemActivityService.getWorkItemActivityListByWorkItemIds(anyCollection(), anyInt())) + .thenReturn(Collections.singletonList(new PmsWorkItemActivityDO().setId(randomLongId()) + .setWorkItemId(pendingWorkItem.getId()).setOperatorUserId(userId).setContent("更新了工作项"))); + + // 调用 + PmsIterationOverviewRespVO overview = iterationService.getIterationOverview(iteration.getId(), userId); + + // 断言 + assertEquals(2, overview.getTotalCount()); + assertEquals(1, overview.getPendingCount()); + assertEquals(1, overview.getCompletedCount()); + assertEquals(50, overview.getProgress()); + assertEquals(1, overview.getTypeStatusCountMap().get(PmsWorkItemTypeEnum.TASK.getType()) + .get(PmsWorkItemStatusTypeEnum.PENDING.getType())); + assertEquals(1, overview.getTypeStatusCountMap().get(PmsWorkItemTypeEnum.DEFECT.getType()) + .get(PmsWorkItemStatusTypeEnum.COMPLETED.getType())); + assertEquals(14, overview.getStatusTrends().size()); + PmsIterationOverviewRespVO.TrendItem todayTrend = overview.getStatusTrends().get(13); + assertEquals(1, todayTrend.getPendingCount()); + assertEquals(1, todayTrend.getCompletedCount()); + assertEquals(5, overview.getBurnDowns().size()); + assertEquals(9, overview.getBurnDowns().get(1).getIdealRemaining()); + assertEquals(9, overview.getBurnDowns().get(2).getActualRemaining()); + assertEquals(1, overview.getRecentActivities().size()); + assertEquals(pendingWorkItem.getName(), CollUtil.getFirst(overview.getRecentActivities()).getWorkItemName()); + } + + // ========== 随机对象 ========== + + private static PmsIterationSaveReqVO randomIterationSaveReqVO(Long projectId) { + LocalDateTime startTime = LocalDateTime.now().plusDays(1); + return randomPojo(PmsIterationSaveReqVO.class, + item -> item.setId(null).setProjectId(projectId).setName("第一期迭代") + .setOwnerUserId(randomLongId()).setTarget("交付第一期").setDescription("迭代说明") + .setStartTime(startTime).setEndTime(startTime.plusDays(14))); + } + + private static PmsIterationDO randomIterationDO(Long projectId, Integer status) { + return randomPojo(PmsIterationDO.class, item -> item.setProjectId(projectId).setName("测试迭代") + .setOwnerUserId(null).setStatus(status).setSort(1).setStartTime(null).setEndTime(null) + .setFinishTime(null)); + } + + private static PmsProjectDO randomProjectDO(Long id) { + return randomPojo(PmsProjectDO.class, project -> project.setId(id).setName("测试项目") + .setStatus(PmsProjectStatusEnum.ACTIVE.getStatus()).setType(1).setLevel(3).setOpenStatus(false)); + } + + private static PmsWorkItemDO randomWorkItemDO(Integer type, Integer status) { + PmsWorkItemDO workItem = randomPojo(PmsWorkItemDO.class, item -> item.setId(randomLongId()).setType(type) + .setStatus(status).setSerialNumber(1).setName("测试工作项")); + workItem.setUpdateTime(LocalDateTime.now()); + return workItem; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementServiceImplTest.java new file mode 100644 index 000000000..06c8c49bc --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectAnnouncementServiceImplTest.java @@ -0,0 +1,133 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.announcement.PmsProjectAnnouncementSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectAnnouncementDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectAnnouncementMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Collections; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * {@link PmsProjectAnnouncementServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsProjectAnnouncementServiceImpl.class) +public class PmsProjectAnnouncementServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsProjectAnnouncementServiceImpl announcementService; + + @Resource + private PmsProjectAnnouncementMapper announcementMapper; + + @MockitoBean + private PmsProjectMemberService projectMemberService; + + @Test + public void testCreateProjectAnnouncement_success() { + // mock 数据 + Long projectId = randomLongId(); + Long userId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(new PmsProjectDO().setId(projectId)); + // 准备参数 + PmsProjectAnnouncementSaveReqVO reqVO = new PmsProjectAnnouncementSaveReqVO() + .setProjectId(projectId).setContent("项目启动") + .setFileUrls(Collections.singletonList("https://example.com/a.pdf")); + + // 调用 + Long id = announcementService.createProjectAnnouncement(reqVO, userId); + + // 断言 + PmsProjectAnnouncementDO announcement = announcementMapper.selectById(id); + assertEquals("项目启动", announcement.getContent()); + } + + @Test + public void testUpdateProjectAnnouncement_success() { + // mock 数据 + Long projectId = randomLongId(); + Long userId = randomLongId(); + PmsProjectAnnouncementDO announcement = new PmsProjectAnnouncementDO() + .setProjectId(projectId).setContent("项目启动"); + announcementMapper.insert(announcement); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(new PmsProjectDO().setId(projectId)); + // 准备参数 + PmsProjectAnnouncementSaveReqVO reqVO = new PmsProjectAnnouncementSaveReqVO() + .setId(announcement.getId()).setProjectId(projectId).setContent("项目已启动") + .setFileUrls(Collections.singletonList("https://example.com/a.pdf")); + + // 调用 + announcementService.updateProjectAnnouncement(reqVO, userId); + + // 断言 + announcement = announcementMapper.selectById(announcement.getId()); + assertEquals("项目已启动", announcement.getContent()); + } + + @Test + public void testDeleteProjectAnnouncement_success() { + // mock 数据 + Long projectId = randomLongId(); + Long userId = randomLongId(); + PmsProjectAnnouncementDO announcement = new PmsProjectAnnouncementDO() + .setProjectId(projectId).setContent("项目启动"); + announcementMapper.insert(announcement); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(new PmsProjectDO().setId(projectId)); + + // 调用 + announcementService.deleteProjectAnnouncement(announcement.getId(), userId); + + // 断言 + assertNull(announcementMapper.selectById(announcement.getId())); + } + + @Test + public void testGetProjectAnnouncement_success() { + // mock 数据 + Long projectId = randomLongId(); + Long userId = randomLongId(); + PmsProjectAnnouncementDO announcement = new PmsProjectAnnouncementDO() + .setProjectId(projectId).setContent("项目启动"); + announcementMapper.insert(announcement); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(new PmsProjectDO().setId(projectId)); + + // 调用 + PmsProjectAnnouncementDO result = announcementService + .getProjectAnnouncement(announcement.getId(), userId); + + // 断言 + assertEquals(announcement.getId(), result.getId()); + assertEquals("项目启动", result.getContent()); + } + + @Test + public void testDeleteProjectAnnouncementListByProjectId() { + // mock 数据 + Long projectId = randomLongId(); + announcementMapper.insert(new PmsProjectAnnouncementDO().setProjectId(projectId).setContent("公告")); + + // 调用 + announcementService.deleteProjectAnnouncementListByProjectId(projectId); + + // 断言 + assertTrue(CollUtil.isEmpty(announcementMapper.selectListByProjectId(projectId))); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteServiceImplTest.java new file mode 100644 index 000000000..d05e223b6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectFavoriteServiceImplTest.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectFavoriteMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * {@link PmsProjectFavoriteServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsProjectFavoriteServiceImpl.class) +public class PmsProjectFavoriteServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsProjectFavoriteServiceImpl favoriteService; + + @Resource + private PmsProjectFavoriteMapper favoriteMapper; + + @MockitoBean + private PmsProjectMemberService projectMemberService; + + @Test + public void testCreateProjectFavorite_success() { + // mock 数据 + Long projectId = randomLongId(); + Long userId = randomLongId(); + when(projectMemberService.validateProjectMember(projectId, userId)) + .thenReturn(new PmsProjectDO().setId(projectId)); + + // 调用 + favoriteService.createProjectFavorite(projectId, userId); + + // 断言 + assertNotNull(favoriteMapper.selectByProjectIdAndUserId(projectId, userId)); + } + + @Test + public void testDeleteProjectFavorite_success() { + // mock 数据 + Long projectId = randomLongId(); + Long userId = randomLongId(); + when(projectMemberService.validateProjectMember(projectId, userId)) + .thenReturn(new PmsProjectDO().setId(projectId)); + favoriteService.createProjectFavorite(projectId, userId); + + // 调用 + favoriteService.deleteProjectFavorite(projectId, userId); + + // 断言 + assertNull(favoriteMapper.selectByProjectIdAndUserId(projectId, userId)); + } + + @Test + public void testDeleteProjectFavoriteListByProjectId() { + // mock 数据 + Long projectId = randomLongId(); + Long userId = randomLongId(); + when(projectMemberService.validateProjectMember(projectId, userId)) + .thenReturn(new PmsProjectDO().setId(projectId)); + favoriteService.createProjectFavorite(projectId, userId); + + // 调用 + favoriteService.deleteProjectFavoriteListByProjectId(projectId); + + // 断言 + assertTrue(CollUtil.isEmpty(favoriteMapper.selectProjectIdListByUserId(userId))); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupServiceImplTest.java new file mode 100644 index 000000000..49a6e84f6 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectGroupServiceImplTest.java @@ -0,0 +1,345 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupMoveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.group.PmsProjectGroupSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectGroupDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectGroupRelationDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectGroupMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectGroupRelationMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectGroupTypeEnum; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_GROUP_DEFAULT_CANNOT_DELETE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_GROUP_DEFAULT_CANNOT_MODIFY; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_GROUP_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_GROUP_NOT_EXISTS; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsProjectGroupServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsProjectGroupServiceImpl.class) +public class PmsProjectGroupServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsProjectGroupServiceImpl projectGroupService; + + @Resource + private PmsProjectGroupMapper projectGroupMapper; + @Resource + private PmsProjectGroupRelationMapper projectGroupRelationMapper; + + @MockitoBean + private PmsProjectMemberService projectMemberService; + + @Test + public void testCreateProjectGroup_success() { + // 准备参数 + Long userId = randomLongId(); + PmsProjectGroupSaveReqVO reqVO = new PmsProjectGroupSaveReqVO().setName("重点项目"); + + // 调用 + Long groupId = projectGroupService.createProjectGroup(reqVO, userId); + + // 断言 + PmsProjectGroupDO group = projectGroupMapper.selectById(groupId); + assertNotNull(group); + assertEquals(userId, group.getUserId()); + assertEquals("重点项目", group.getName()); + assertEquals(2, group.getSort()); + assertEquals(PmsProjectGroupTypeEnum.CUSTOM.getType(), group.getType()); + assertEquals(1, projectGroupMapper.selectListByUserId(userId).size()); + } + + @Test + public void testCreateProjectGroup_nameDuplicate() { + // mock 数据 + Long userId = randomLongId(); + projectGroupMapper.insert(randomProjectGroupDO(userId, "重点项目", 2, + PmsProjectGroupTypeEnum.CUSTOM.getType())); + // 准备参数 + PmsProjectGroupSaveReqVO reqVO = new PmsProjectGroupSaveReqVO().setName("重点项目"); + + // 调用,并断言异常 + assertServiceException(() -> projectGroupService.createProjectGroup(reqVO, userId), + PROJECT_GROUP_NAME_DUPLICATE); + } + + @Test + public void testUpdateProjectGroup_success() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO group = randomProjectGroupDO(userId, "旧名称", 2, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(group); + // 准备参数 + PmsProjectGroupSaveReqVO reqVO = new PmsProjectGroupSaveReqVO() + .setId(group.getId()).setName("新名称"); + + // 调用 + projectGroupService.updateProjectGroup(reqVO, userId); + + // 断言 + assertEquals("新名称", projectGroupMapper.selectById(group.getId()).getName()); + } + + @Test + public void testUpdateProjectGroup_defaultCannotModify() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO group = randomProjectGroupDO(userId, "全部项目", 0, + PmsProjectGroupTypeEnum.ALL.getType()); + projectGroupMapper.insert(group); + // 准备参数 + PmsProjectGroupSaveReqVO reqVO = new PmsProjectGroupSaveReqVO() + .setId(group.getId()).setName("新名称"); + + // 调用,并断言异常 + assertServiceException(() -> projectGroupService.updateProjectGroup(reqVO, userId), + PROJECT_GROUP_DEFAULT_CANNOT_MODIFY); + } + + @Test + public void testUpdateProjectGroupSort_success() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO firstGroup = randomProjectGroupDO(userId, "分组一", 0, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(firstGroup); + PmsProjectGroupDO secondGroup = randomProjectGroupDO(userId, "分组二", 1, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(secondGroup); + // 准备参数 + PmsProjectGroupSortReqVO reqVO = new PmsProjectGroupSortReqVO().setItems(asList( + new PmsProjectGroupSortReqVO.Item().setId(firstGroup.getId()).setSort(2), + new PmsProjectGroupSortReqVO.Item().setId(secondGroup.getId()).setSort(1))); + + // 调用 + projectGroupService.updateProjectGroupSort(reqVO, userId); + + // 断言 + assertEquals(2, projectGroupMapper.selectById(firstGroup.getId()).getSort()); + assertEquals(1, projectGroupMapper.selectById(secondGroup.getId()).getSort()); + } + + @Test + public void testUpdateProjectGroupSort_notOwner() { + // mock 数据 + PmsProjectGroupDO group = randomProjectGroupDO(randomLongId(), "其他用户分组", 0, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(group); + // 准备参数 + PmsProjectGroupSortReqVO reqVO = new PmsProjectGroupSortReqVO().setItems(asList( + new PmsProjectGroupSortReqVO.Item().setId(group.getId()).setSort(1))); + + // 调用,并断言异常 + assertServiceException(() -> projectGroupService.updateProjectGroupSort(reqVO, randomLongId()), + PROJECT_GROUP_NOT_EXISTS); + } + + @Test + public void testDeleteProjectGroup_success() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO group = randomProjectGroupDO(userId, "测试分组", 2, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(group); + PmsProjectGroupRelationDO relation = randomProjectGroupRelationDO(userId, group.getId(), randomLongId()); + projectGroupRelationMapper.insert(relation); + + // 调用 + projectGroupService.deleteProjectGroup(group.getId(), userId); + + // 断言 + assertNull(projectGroupMapper.selectById(group.getId())); + assertNull(projectGroupRelationMapper.selectById(relation.getId())); + } + + @Test + public void testDeleteProjectGroup_defaultCannotDelete() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO group = randomProjectGroupDO(userId, "未分组", 1, + PmsProjectGroupTypeEnum.UNGROUPED.getType()); + projectGroupMapper.insert(group); + + // 调用,并断言异常 + assertServiceException(() -> projectGroupService.deleteProjectGroup(group.getId(), userId), + PROJECT_GROUP_DEFAULT_CANNOT_DELETE); + } + + @Test + public void testGetProjectGroupList_noRelations() { + // mock 数据 + Long userId = randomLongId(); + + // 调用 + List groups = projectGroupService.getProjectGroupList(userId); + Map groupCountMap = projectGroupService.getProjectGroupCountMap( + userId, groups, asList(101L, 102L)); + + // 断言 + PmsProjectGroupDO allGroup = CollUtil.getFirst(groups); + PmsProjectGroupDO ungroupedGroup = groups.get(1); + assertEquals(2, groupCountMap.get(allGroup.getId())); + assertEquals(2, groupCountMap.get(ungroupedGroup.getId())); + } + + @Test + public void testGetProjectGroupList_customGroupCounts() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO customGroup = randomProjectGroupDO(userId, "重点项目", 2, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(customGroup); + projectGroupRelationMapper.insert(randomProjectGroupRelationDO(userId, customGroup.getId(), 101L)); + + // 调用 + List groups = projectGroupService.getProjectGroupList(userId); + Map groupCountMap = projectGroupService.getProjectGroupCountMap( + userId, groups, asList(101L, 102L, 103L)); + + // 断言 + assertEquals(3, groupCountMap.get(CollUtil.getFirst(groups).getId())); + assertEquals(2, groupCountMap.get(groups.get(1).getId())); + assertEquals(1, groupCountMap.get(customGroup.getId())); + } + + @Test + public void testGetProjectGroupList_sameProjectInMultipleGroups() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO firstGroup = randomProjectGroupDO(userId, "重点项目", 2, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(firstGroup); + PmsProjectGroupDO secondGroup = randomProjectGroupDO(userId, "产品项目", 3, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(secondGroup); + projectGroupRelationMapper.insert(randomProjectGroupRelationDO(userId, firstGroup.getId(), 101L)); + projectGroupRelationMapper.insert(randomProjectGroupRelationDO(userId, secondGroup.getId(), 101L)); + + // 调用 + List groups = projectGroupService.getProjectGroupList(userId); + Map groupCountMap = projectGroupService.getProjectGroupCountMap( + userId, groups, asList(101L, 102L)); + + // 断言 + assertEquals(2, groupCountMap.get(CollUtil.getFirst(groups).getId())); + assertEquals(1, groupCountMap.get(groups.get(1).getId())); + assertEquals(1, groupCountMap.get(firstGroup.getId())); + assertEquals(1, groupCountMap.get(secondGroup.getId())); + } + + @Test + public void testMoveProjectToGroup_createAndUpdate() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsProjectGroupDO firstGroup = randomProjectGroupDO(userId, "分组一", 2, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(firstGroup); + PmsProjectGroupDO secondGroup = randomProjectGroupDO(userId, "分组二", 3, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(secondGroup); + + // 调用 + projectGroupService.moveProjectToGroup(new PmsProjectGroupMoveReqVO() + .setProjectId(projectId).setGroupId(firstGroup.getId()), userId); + projectGroupService.moveProjectToGroup(new PmsProjectGroupMoveReqVO() + .setProjectId(projectId).setGroupId(secondGroup.getId()), userId); + + // 断言 + PmsProjectGroupRelationDO relation = + projectGroupRelationMapper.selectByUserIdAndProjectId(userId, projectId); + assertNotNull(relation); + assertEquals(secondGroup.getId(), relation.getGroupId()); + verify(projectMemberService, times(2)).validateProjectMember(projectId, userId); + } + + @Test + public void testMoveProjectToGroup_allCannotMove() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO group = randomProjectGroupDO(userId, "全部项目", 0, + PmsProjectGroupTypeEnum.ALL.getType()); + projectGroupMapper.insert(group); + // 准备参数 + PmsProjectGroupMoveReqVO reqVO = new PmsProjectGroupMoveReqVO() + .setProjectId(randomLongId()).setGroupId(group.getId()); + + // 调用,并断言异常 + assertServiceException(() -> projectGroupService.moveProjectToGroup(reqVO, userId), + PROJECT_GROUP_DEFAULT_CANNOT_MODIFY); + } + + @Test + public void testFilterProjectIdListByGroup_ungrouped() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO ungrouped = randomProjectGroupDO(userId, "未分组", 1, + PmsProjectGroupTypeEnum.UNGROUPED.getType()); + projectGroupMapper.insert(ungrouped); + PmsProjectGroupDO customGroup = randomProjectGroupDO(userId, "重点项目", 2, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(customGroup); + projectGroupRelationMapper.insert(randomProjectGroupRelationDO(userId, customGroup.getId(), 101L)); + + // 调用 + List projectIds = projectGroupService.filterProjectIdListByGroupId( + ungrouped.getId(), userId, asList(101L, 102L)); + + // 断言 + assertEquals(asList(102L), projectIds); + } + + @Test + public void testFilterProjectIdListByGroup_customGroup() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectGroupDO customGroup = randomProjectGroupDO(userId, "重点项目", 2, + PmsProjectGroupTypeEnum.CUSTOM.getType()); + projectGroupMapper.insert(customGroup); + projectGroupRelationMapper.insert(randomProjectGroupRelationDO(userId, customGroup.getId(), 101L)); + + // 调用 + List projectIds = projectGroupService.filterProjectIdListByGroupId( + customGroup.getId(), userId, asList(101L, 102L)); + + // 断言 + assertEquals(asList(101L), projectIds); + } + + // ========== 随机对象 ========== + + private static PmsProjectGroupDO randomProjectGroupDO(Long userId, String name, Integer sort, Integer type) { + return randomPojo(PmsProjectGroupDO.class, group -> group.setUserId(userId) + .setName(name).setSort(sort).setType(type)); + } + + private static PmsProjectGroupRelationDO randomProjectGroupRelationDO( + Long userId, Long groupId, Long projectId) { + return randomPojo(PmsProjectGroupRelationDO.class, relation -> relation.setUserId(userId) + .setGroupId(groupId).setProjectId(projectId).setSort(0)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberServiceImplTest.java new file mode 100644 index 000000000..3397c9e8e --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectMemberServiceImplTest.java @@ -0,0 +1,358 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member.PmsProjectMemberRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.member.PmsProjectMemberSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectMemberDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectMemberMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectMemberLevelEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_MEMBER_CREATOR_CANNOT_REMOVE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_MEMBER_OWNER_CANNOT_EXIT; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_MEMBER_OWNER_LEVEL_CANNOT_ASSIGN; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_WRITE_ACCESS_DENIED; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * {@link PmsProjectMemberServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsProjectMemberServiceImpl.class) +public class PmsProjectMemberServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsProjectMemberServiceImpl projectMemberService; + + @Resource + private PmsProjectMemberMapper projectMemberMapper; + + @MockitoBean + private PmsProjectService projectService; + @MockitoBean + private AdminUserApi adminUserApi; + @MockitoBean + private PermissionApi permissionApi; + + @BeforeEach + public void setUpCloudApiMocks() { + lenient().when(permissionApi.hasAnyRoles(anyLong(), any(String[].class))).thenReturn(success(false)); + lenient().when(adminUserApi.validateUserList(anyCollection())).thenReturn(success(true)); + } + + @Test + public void testCreateProjectMemberList_success() { + // 准备参数 + Long projectId = randomLongId(); + Long creatorId = randomLongId(); + Long memberId = randomLongId(); + + // 调用 + projectMemberService.createProjectMemberList(projectId, creatorId, asList(creatorId, memberId)); + + // 断言 + List members = projectMemberMapper.selectListByProjectId(projectId); + assertEquals(2, members.size()); + assertTrue(members.stream().anyMatch(member -> creatorId.equals(member.getUserId()) + && PmsProjectMemberLevelEnum.OWNER.getLevel().equals(member.getLevel()))); + assertTrue(members.stream().anyMatch(member -> memberId.equals(member.getUserId()) + && PmsProjectMemberLevelEnum.WRITE.getLevel().equals(member.getLevel()))); + } + + @Test + public void testSaveProjectMembers_success() { + // mock 数据 + Long projectId = randomLongId(); + Long creatorId = randomLongId(); + Long operatorUserId = randomLongId(); + Long memberUserId = randomLongId(); + PmsProjectDO project = randomProjectDO(projectId, creatorId); + when(projectService.getProject(projectId)).thenReturn(project); + projectMemberMapper.insert(randomProjectMemberDO(projectId, operatorUserId, + PmsProjectMemberLevelEnum.ADMIN)); + projectMemberMapper.insert(randomProjectMemberDO(projectId, memberUserId, + PmsProjectMemberLevelEnum.READ)); + // 准备参数 + PmsProjectMemberSaveReqVO reqVO = new PmsProjectMemberSaveReqVO().setProjectId(projectId) + .setMembers(singletonList(new PmsProjectMemberSaveReqVO.Member().setUserId(memberUserId) + .setLevel(PmsProjectMemberLevelEnum.WRITE.getLevel()))); + + // 调用 + projectMemberService.updateProjectMemberList(reqVO, operatorUserId); + + // 断言 + PmsProjectMemberDO member = projectMemberMapper.selectByProjectIdAndUserId(projectId, memberUserId); + assertEquals(PmsProjectMemberLevelEnum.WRITE.getLevel(), member.getLevel()); + } + + @Test + public void testSaveProjectMembers_ownerLevelCannotAssign() { + // mock 数据 + Long projectId = randomLongId(); + Long creatorId = randomLongId(); + Long operatorUserId = randomLongId(); + when(projectService.getProject(projectId)).thenReturn(randomProjectDO(projectId, creatorId)); + projectMemberMapper.insert(randomProjectMemberDO(projectId, operatorUserId, + PmsProjectMemberLevelEnum.ADMIN)); + // 准备参数 + PmsProjectMemberSaveReqVO reqVO = new PmsProjectMemberSaveReqVO().setProjectId(projectId) + .setMembers(singletonList(new PmsProjectMemberSaveReqVO.Member().setUserId(randomLongId()) + .setLevel(PmsProjectMemberLevelEnum.OWNER.getLevel()))); + + // 调用,并断言异常 + assertServiceException(() -> projectMemberService.updateProjectMemberList(reqVO, operatorUserId), + PROJECT_MEMBER_OWNER_LEVEL_CANNOT_ASSIGN); + } + + @Test + public void testDeleteProjectMember_ownerCannotRemove() { + // mock 数据 + Long projectId = randomLongId(); + Long creatorId = randomLongId(); + PmsProjectDO project = randomProjectDO(projectId, creatorId); + when(projectService.getProject(projectId)).thenReturn(project); + projectMemberMapper.insert(randomProjectMemberDO(projectId, creatorId, + PmsProjectMemberLevelEnum.OWNER)); + + // 调用,并断言异常 + assertServiceException(() -> projectMemberService.deleteProjectMember(projectId, creatorId, creatorId), + PROJECT_MEMBER_CREATOR_CANNOT_REMOVE); + } + + @Test + public void testExitProject_success() { + // mock 数据 + Long projectId = randomLongId(); + Long creatorId = randomLongId(); + Long memberId = randomLongId(); + when(projectService.getProject(projectId)).thenReturn(randomProjectDO(projectId, creatorId)); + projectMemberMapper.insert(randomProjectMemberDO(projectId, memberId, + PmsProjectMemberLevelEnum.WRITE)); + + // 调用 + projectMemberService.exitProject(projectId, memberId); + + // 断言 + assertNull(projectMemberMapper.selectByProjectIdAndUserId(projectId, memberId)); + } + + @Test + public void testExitProject_ownerCannotExit() { + // mock 数据 + Long projectId = randomLongId(); + Long ownerId = randomLongId(); + when(projectService.getProject(projectId)).thenReturn(randomProjectDO(projectId, ownerId)); + projectMemberMapper.insert(randomProjectMemberDO(projectId, ownerId, + PmsProjectMemberLevelEnum.OWNER)); + + // 调用,并断言异常 + assertServiceException(() -> projectMemberService.exitProject(projectId, ownerId), + PROJECT_MEMBER_OWNER_CANNOT_EXIT); + } + + @Test + public void testGetProjectMemberList_success() { + // mock 数据 + Long projectId = randomLongId(); + Long creatorId = randomLongId(); + Long memberId = randomLongId(); + PmsProjectDO project = randomProjectDO(projectId, creatorId); + when(projectService.getProject(projectId)).thenReturn(project); + projectMemberMapper.insert(randomProjectMemberDO(projectId, creatorId, + PmsProjectMemberLevelEnum.OWNER)); + projectMemberMapper.insert(randomProjectMemberDO(projectId, memberId, + PmsProjectMemberLevelEnum.WRITE)); + + // 调用 + List members = projectMemberService.getProjectMemberList(projectId, creatorId); + + // 断言 + assertEquals(2, members.size()); + PmsProjectMemberRespVO member = CollUtil.findOne(members, item -> memberId.equals(item.getUserId())); + assertEquals(PmsProjectMemberLevelEnum.WRITE.getLevel(), member.getLevel()); + } + + @Test + public void testGetActiveProjectIdList() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO activeProject = randomProjectDO("进行中项目", PmsProjectStatusEnum.ACTIVE.getStatus()); + PmsProjectDO archivedProject = randomProjectDO("已归档项目", PmsProjectStatusEnum.ARCHIVED.getStatus()); + projectMemberMapper.insert(randomProjectMemberDO(activeProject.getId(), userId)); + projectMemberMapper.insert(randomProjectMemberDO(activeProject.getId(), userId)); + projectMemberMapper.insert(randomProjectMemberDO(archivedProject.getId(), userId)); + when(projectService.getProjectList(anyCollection())).thenReturn(asList(activeProject, archivedProject)); + + // 调用 + List projectIds = projectMemberService.getActiveProjectIdListByUserId(userId); + + // 断言 + assertEquals(singletonList(activeProject.getId()), projectIds); + } + + @Test + public void testGetActiveProjectIdList_empty() { + // 调用 + List projectIds = projectMemberService.getActiveProjectIdListByUserId(randomLongId()); + + // 断言 + assertTrue(CollUtil.isEmpty(projectIds)); + } + + @Test + public void testValidateProjectMember_success() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO("测试项目", PmsProjectStatusEnum.ACTIVE.getStatus()); + when(projectService.getProject(project.getId())).thenReturn(project); + projectMemberMapper.insert(randomProjectMemberDO(project.getId(), userId)); + + // 调用 + PmsProjectDO memberProject = projectMemberService.validateProjectMember(project.getId(), userId); + + // 断言 + assertEquals(project.getId(), memberProject.getId()); + } + + @Test + public void testValidateProjectMember_notExists() { + // 调用,并断言异常 + assertServiceException(() -> projectMemberService.validateProjectMember(randomLongId(), randomLongId()), + PROJECT_NOT_EXISTS); + } + + @Test + public void testValidateProjectMember_accessDenied() { + // mock 数据 + PmsProjectDO project = randomProjectDO("测试项目", PmsProjectStatusEnum.ACTIVE.getStatus()); + when(projectService.getProject(project.getId())).thenReturn(project); + + // 调用,并断言异常 + assertServiceException(() -> projectMemberService.validateProjectMember(project.getId(), randomLongId()), + PROJECT_ACCESS_DENIED); + } + + @Test + public void testValidateProjectReadable_publicProject() { + // mock 数据 + PmsProjectDO project = randomProjectDO("公开项目", PmsProjectStatusEnum.ACTIVE.getStatus()) + .setOpenStatus(true); + when(projectService.getProject(project.getId())).thenReturn(project); + + // 调用 + PmsProjectDO readableProject = projectMemberService.validateProjectReadable(project.getId(), randomLongId()); + + // 断言 + assertEquals(project.getId(), readableProject.getId()); + } + + @Test + public void testValidateProjectReadable_privateProjectAccessDenied() { + // mock 数据 + PmsProjectDO project = randomProjectDO("私有项目", PmsProjectStatusEnum.ACTIVE.getStatus()) + .setOpenStatus(false); + when(projectService.getProject(project.getId())).thenReturn(project); + + // 调用,并断言异常 + assertServiceException(() -> projectMemberService.validateProjectReadable(project.getId(), randomLongId()), + PROJECT_ACCESS_DENIED); + } + + @Test + public void testValidateProjectReadable_superAdmin() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO("私有项目", PmsProjectStatusEnum.ACTIVE.getStatus()) + .setOpenStatus(false); + when(projectService.getProject(project.getId())).thenReturn(project); + when(permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode())).thenReturn(success(true)); + + // 调用 + PmsProjectDO readableProject = projectMemberService.validateProjectReadable(project.getId(), userId); + + // 断言 + assertEquals(project.getId(), readableProject.getId()); + } + + @Test + public void testValidateProjectWritable_writeLevel() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO("可编辑项目", PmsProjectStatusEnum.ACTIVE.getStatus()); + when(projectService.getProject(project.getId())).thenReturn(project); + projectMemberMapper.insert(randomProjectMemberDO(project.getId(), userId) + .setLevel(PmsProjectMemberLevelEnum.WRITE.getLevel())); + + // 调用 + PmsProjectDO writableProject = projectMemberService.validateProjectWritable(project.getId(), userId); + + // 断言 + assertEquals(project.getId(), writableProject.getId()); + } + + @Test + public void testValidateProjectWritable_readLevel() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO("只读项目", PmsProjectStatusEnum.ACTIVE.getStatus()); + when(projectService.getProject(project.getId())).thenReturn(project); + projectMemberMapper.insert(randomProjectMemberDO(project.getId(), userId) + .setLevel(PmsProjectMemberLevelEnum.READ.getLevel())); + + // 调用,并断言异常 + assertServiceException(() -> projectMemberService.validateProjectWritable(project.getId(), userId), + PROJECT_WRITE_ACCESS_DENIED); + } + + // ========== 随机对象 ========== + + private static PmsProjectDO randomProjectDO(Long projectId, Long creatorId) { + return randomPojo(PmsProjectDO.class, project -> project.setId(projectId).setName("测试项目") + .setStatus(1).setType(1).setLevel(3).setOpenStatus(false).setCreator(String.valueOf(creatorId))); + } + + private static PmsProjectMemberDO randomProjectMemberDO(Long projectId, Long userId, + PmsProjectMemberLevelEnum level) { + return randomPojo(PmsProjectMemberDO.class, member -> member.setProjectId(projectId) + .setUserId(userId).setLevel(level.getLevel())); + } + + private static PmsProjectDO randomProjectDO(String name, Integer status) { + return randomPojo(PmsProjectDO.class, project -> project.setName(name).setStatus(status) + .setType(1).setLevel(3).setOpenStatus(true)); + } + + private static PmsProjectMemberDO randomProjectMemberDO(Long projectId, Long userId) { + return randomPojo(PmsProjectMemberDO.class, member -> member.setProjectId(projectId) + .setUserId(userId).setLevel(PmsProjectMemberLevelEnum.WRITE.getLevel())); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImplTest.java new file mode 100644 index 000000000..37274e57b --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImplTest.java @@ -0,0 +1,333 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectOverviewRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.project.PmsProjectSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectSceneTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectSortTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemStatusService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemWorkLogService; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ADMIN_REQUIRED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_OWNER_REQUIRED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsProjectServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsProjectServiceImpl.class) +public class PmsProjectServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsProjectServiceImpl projectService; + + @Resource + private PmsProjectMapper projectMapper; + + @MockitoBean + private PmsProjectMemberService projectMemberService; + @MockitoBean + private PmsProjectGroupService projectGroupService; + @MockitoBean + private PmsProjectFavoriteService projectFavoriteService; + @MockitoBean + private PmsProjectAnnouncementService projectAnnouncementService; + @MockitoBean + private PmsIterationService iterationService; + @MockitoBean + private PmsWorkItemService workItemService; + @MockitoBean + private PmsWorkItemStatusService workItemStatusService; + @MockitoBean + private PmsWorkItemWorkLogService workItemWorkLogService; + @MockitoBean + private PermissionApi permissionApi; + + @BeforeEach + public void setUpCloudApiMocks() { + lenient().when(permissionApi.hasAnyRoles(anyLong(), any(String[].class))).thenReturn(success(false)); + } + + @Test + public void testCreateProject_success() { + // 准备参数 + Long userId = randomLongId(); + PmsProjectSaveReqVO reqVO = randomProjectSaveReqVO(); + reqVO.setMemberUserIds(asList(randomLongId(), randomLongId())); + + // 调用 + Long projectId = projectService.createProject(reqVO, userId); + + // 断言 + PmsProjectDO project = projectMapper.selectById(projectId); + assertNotNull(project); + assertEquals(reqVO.getName(), project.getName()); + assertEquals(PmsProjectStatusEnum.ACTIVE.getStatus(), project.getStatus()); + verify(projectMemberService).createProjectMemberList(projectId, userId, reqVO.getMemberUserIds()); + verify(workItemStatusService).initProjectWorkItemStatuses(projectId, reqVO.getType()); + } + + @Test + public void testUpdateProject_notAdmin() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()); + projectMapper.insert(project); + when(projectMemberService.hasProjectManagerPermission(project.getId(), userId)).thenReturn(false); + // 准备参数 + PmsProjectSaveReqVO reqVO = randomProjectSaveReqVO(project.getId()); + + // 调用,并断言异常 + assertServiceException(() -> projectService.updateProject(reqVO, userId), PROJECT_ADMIN_REQUIRED); + } + + @Test + public void testGetProjectPage_allIncludesOpenProject() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO openProject = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()).setOpenStatus(true); + projectMapper.insert(openProject); + when(projectMemberService.getProjectIdListByUserId(userId)).thenReturn(emptyList()); + // 准备参数 + PmsProjectPageReqVO reqVO = new PmsProjectPageReqVO() + .setSceneType(PmsProjectSceneTypeEnum.ALL.getType()) + .setStatus(PmsProjectStatusEnum.ACTIVE.getStatus()); + + // 调用 + PageResult pageResult = projectService.getProjectPage(reqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(openProject.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetProjectPage_createTimeAscending() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO oldProject = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()).setOpenStatus(true); + oldProject.setCreateTime(LocalDateTime.now().minusDays(2)); + PmsProjectDO newProject = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()).setOpenStatus(true); + newProject.setCreateTime(LocalDateTime.now().minusDays(1)); + projectMapper.insert(oldProject); + projectMapper.insert(newProject); + when(projectMemberService.getProjectIdListByUserId(userId)).thenReturn(emptyList()); + // 准备参数 + PmsProjectPageReqVO reqVO = new PmsProjectPageReqVO() + .setSceneType(PmsProjectSceneTypeEnum.ALL.getType()) + .setStatus(PmsProjectStatusEnum.ACTIVE.getStatus()) + .setSortType(PmsProjectSortTypeEnum.CREATE_TIME.getType()); + + // 调用 + PageResult pageResult = projectService.getProjectPage(reqVO, userId); + + // 断言:创建时间按升序,旧项目在前 + assertEquals(Arrays.asList(oldProject.getId(), newProject.getId()), + convertList(pageResult.getList(), PmsProjectDO::getId)); + } + + @Test + public void testGetFavoriteProjectList_success() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO favoriteProject = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()); + projectMapper.insert(favoriteProject); + PmsProjectDO nonMemberProject = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()); + projectMapper.insert(nonMemberProject); + PmsProjectDO archivedProject = randomProjectDO(PmsProjectStatusEnum.ARCHIVED.getStatus()); + projectMapper.insert(archivedProject); + when(projectFavoriteService.getFavoriteProjectIdListByUserId(userId)) + .thenReturn(asList(favoriteProject.getId(), nonMemberProject.getId(), archivedProject.getId())); + when(projectMemberService.getProjectIdListByUserId(userId)) + .thenReturn(asList(favoriteProject.getId(), archivedProject.getId())); + + // 调用 + List projects = projectService.getFavoriteProjectList(userId); + + // 断言 + assertEquals(1, projects.size()); + assertEquals(favoriteProject.getId(), CollUtil.getFirst(projects).getId()); + } + + @Test + public void testArchiveProject_success() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()); + projectMapper.insert(project); + when(projectMemberService.validateProjectReadable(project.getId(), userId)).thenReturn(project); + when(projectMemberService.hasProjectManagerPermission(project.getId(), userId)).thenReturn(true); + + // 调用 + projectService.archiveProject(project.getId(), userId); + + // 断言 + PmsProjectDO archivedProject = projectMapper.selectById(project.getId()); + assertEquals(PmsProjectStatusEnum.ARCHIVED.getStatus(), archivedProject.getStatus()); + assertNotNull(archivedProject.getArchiveTime()); + verify(projectGroupService).deleteProjectGroupRelationListByProjectId(project.getId()); + verify(projectFavoriteService).deleteProjectFavoriteListByProjectId(project.getId()); + } + + @Test + public void testRecycleProject_success() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()); + projectMapper.insert(project); + when(projectMemberService.hasProjectManagerPermission(project.getId(), userId)).thenReturn(true); + + // 调用 + projectService.recycleProject(project.getId(), userId); + + // 断言 + PmsProjectDO recycledProject = projectMapper.selectById(project.getId()); + assertEquals(PmsProjectStatusEnum.RECYCLED.getStatus(), recycledProject.getStatus()); + assertNotNull(recycledProject.getRecycleTime()); + verify(projectGroupService).deleteProjectGroupRelationListByProjectId(project.getId()); + verify(projectFavoriteService).deleteProjectFavoriteListByProjectId(project.getId()); + } + + @Test + public void testRestoreProject_success() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO(PmsProjectStatusEnum.ARCHIVED.getStatus()) + .setArchiveTime(LocalDateTime.now()); + projectMapper.insert(project); + when(projectMemberService.validateProjectReadable(project.getId(), userId)).thenReturn(project); + when(projectMemberService.hasProjectManagerPermission(project.getId(), userId)).thenReturn(true); + + // 调用 + projectService.restoreProject(project.getId(), userId); + + // 断言 + PmsProjectDO restoredProject = projectMapper.selectById(project.getId()); + assertEquals(PmsProjectStatusEnum.ACTIVE.getStatus(), restoredProject.getStatus()); + assertNull(restoredProject.getArchiveTime()); + assertNull(restoredProject.getRecycleTime()); + } + + @Test + public void testDeleteProject_statusInvalid() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO(PmsProjectStatusEnum.ACTIVE.getStatus()); + projectMapper.insert(project); + when(projectMemberService.hasProjectOwnerPermission(project.getId(), userId)).thenReturn(true); + + // 调用,并断言异常 + assertServiceException(() -> projectService.deleteProject(project.getId(), userId), PROJECT_STATUS_INVALID); + } + + @Test + public void testDeleteProject_ownerRequired() { + // mock 数据 + Long userId = randomLongId(); + PmsProjectDO project = randomProjectDO(PmsProjectStatusEnum.RECYCLED.getStatus()); + projectMapper.insert(project); + when(projectMemberService.hasProjectOwnerPermission(project.getId(), userId)).thenReturn(false); + + // 调用,并断言异常 + assertServiceException(() -> projectService.deleteProject(project.getId(), userId), PROJECT_OWNER_REQUIRED); + } + + @Test + public void testGetProjectOverview_success() { + // mock 数据 + Long projectId = randomLongId(); + Long userId = randomLongId(); + PmsWorkItemDO pendingWorkItem = randomWorkItemDO(userId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), 1); + PmsWorkItemDO processingWorkItem = randomWorkItemDO(userId, PmsWorkItemTypeEnum.DEFECT.getType(), + PmsWorkItemStatusTypeEnum.PROCESSING.getType(), 2); + PmsWorkItemDO completedWorkItem = randomWorkItemDO(randomLongId(), + PmsWorkItemTypeEnum.REQUIREMENT.getType(), PmsWorkItemStatusTypeEnum.COMPLETED.getType(), 3); + when(workItemService.getActiveWorkItemListByProjectId(projectId)) + .thenReturn(Arrays.asList(pendingWorkItem, processingWorkItem, completedWorkItem)); + + // 调用 + PmsProjectOverviewRespVO overview = projectService.getProjectOverview(projectId, userId); + + // 断言 + assertEquals(3L, overview.getTotalCount()); + assertEquals(1L, overview.getPendingCount()); + assertEquals(1L, overview.getProcessingCount()); + assertEquals(1L, overview.getCompletedCount()); + assertEquals(1L, overview.getTypeCountMap().get(PmsWorkItemTypeEnum.TASK.getType())); + assertEquals(14, overview.getCompletedTrends().size()); + assertEquals(2, overview.getAssignedWorkItems().size()); + verify(projectMemberService).validateProjectReadable(projectId, userId); + } + + // ========== 随机对象 ========== + + private static PmsProjectSaveReqVO randomProjectSaveReqVO() { + LocalDateTime startTime = LocalDateTime.now().plusDays(1); + return randomPojo(PmsProjectSaveReqVO.class, + reqVO -> reqVO.setId(null).setType(1).setMemberUserIds(emptyList()) + .setName("官网重构").setLevel(3).setDescription("测试项目").setOpenStatus(false) + .setIcon("ep:folder").setStartTime(startTime).setEndTime(startTime.plusMonths(1))); + } + + private static PmsProjectSaveReqVO randomProjectSaveReqVO(Long id) { + LocalDateTime startTime = LocalDateTime.now().plusDays(1); + return randomPojo(PmsProjectSaveReqVO.class, reqVO -> reqVO.setId(id).setType(1) + .setMemberUserIds(emptyList()).setName("新项目名称").setLevel(3) + .setDescription("更新描述").setOpenStatus(true).setIcon("ep:folder") + .setStartTime(startTime).setEndTime(startTime.plusMonths(1))); + } + + private static PmsProjectDO randomProjectDO(Integer status) { + return randomPojo(PmsProjectDO.class, project -> project.setName("测试项目").setStatus(status) + .setType(1).setLevel(3).setOpenStatus(false).setCreator(String.valueOf(randomLongId()))); + } + + private static PmsWorkItemDO randomWorkItemDO(Long assigneeUserId, Integer type, Integer status, Integer serial) { + PmsWorkItemDO workItem = randomPojo(PmsWorkItemDO.class, + item -> item.setId(randomLongId()).setAssigneeUserId(assigneeUserId) + .setType(type).setStatus(status).setStatusId(randomLongId()).setSerialNumber(serial) + .setName("测试工作项").setProgress(0).setSort(serial)); + workItem.setUpdateTime(LocalDateTime.now()); + return workItem; + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateServiceImplTest.java new file mode 100644 index 000000000..2bec6b0db --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectTemplateServiceImplTest.java @@ -0,0 +1,138 @@ +package cn.iocoder.yudao.module.pms.service.pm.project; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.project.vo.template.PmsProjectTemplateSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectTemplateDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectTemplateMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import static cn.iocoder.yudao.framework.common.enums.CommonStatusEnum.ENABLE; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_TEMPLATE_CONFIG_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_TEMPLATE_NAME_DUPLICATE; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * {@link PmsProjectTemplateServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsProjectTemplateServiceImpl.class) +public class PmsProjectTemplateServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsProjectTemplateServiceImpl projectTemplateService; + + @Resource + private PmsProjectTemplateMapper projectTemplateMapper; + + @Test + public void testCreateProjectTemplate_success() { + // 准备参数 + PmsProjectTemplateSaveReqVO reqVO = buildProjectTemplateSaveReqVO("通用项目模板-创建"); + + // 调用 + Long templateId = projectTemplateService.createProjectTemplate(reqVO); + + // 断言 + PmsProjectTemplateDO template = projectTemplateMapper.selectById(templateId); + assertNotNull(template); + assertEquals("通用项目模板-创建", template.getName()); + assertEquals(singletonList(PmsWorkItemTypeEnum.TASK.getType()), template.getItemTypes()); + assertEquals("task_todo", template.getStatuses().get(0).getCode()); + assertEquals("todo", template.getBoards().get(0).getCode()); + } + + @Test + public void testCreateProjectTemplate_defaultStatusMissing() { + // 准备参数 + PmsProjectTemplateSaveReqVO reqVO = buildProjectTemplateSaveReqVO("通用项目模板-缺少初始状态"); + reqVO.getStatuses().get(0).setDefaultStatus(false); + + // 调用,并断言异常 + assertServiceException(() -> projectTemplateService.createProjectTemplate(reqVO), + PROJECT_TEMPLATE_CONFIG_INVALID, "每种工作项必须且只能配置一个初始状态"); + } + + @Test + public void testCreateProjectTemplate_nameDuplicate() { + // mock 数据 + projectTemplateService.createProjectTemplate(buildProjectTemplateSaveReqVO("通用项目模板-重名")); + + // 准备参数 + PmsProjectTemplateSaveReqVO reqVO = buildProjectTemplateSaveReqVO("通用项目模板-重名"); + + // 调用,并断言异常 + assertServiceException(() -> projectTemplateService.createProjectTemplate(reqVO), + PROJECT_TEMPLATE_NAME_DUPLICATE); + } + + @Test + public void testUpdateProjectTemplate_success() { + // mock 数据 + Long templateId = projectTemplateService.createProjectTemplate( + buildProjectTemplateSaveReqVO("通用项目模板-修改前")); + + // 准备参数 + PmsProjectTemplateSaveReqVO reqVO = buildProjectTemplateSaveReqVO("通用项目模板-修改后") + .setId(templateId).setDescription("更新后的通用协作模板"); + + // 调用 + projectTemplateService.updateProjectTemplate(reqVO); + + // 断言 + PmsProjectTemplateDO template = projectTemplateMapper.selectById(templateId); + assertNotNull(template); + assertEquals("通用项目模板-修改后", template.getName()); + assertEquals("更新后的通用协作模板", template.getDescription()); + } + + @Test + public void testDeleteProjectTemplate_success() { + // mock 数据 + Long templateId = projectTemplateService.createProjectTemplate( + buildProjectTemplateSaveReqVO("通用项目模板-删除")); + + // 调用 + projectTemplateService.deleteProjectTemplate(templateId); + + // 断言 + assertNull(projectTemplateMapper.selectById(templateId)); + } + + /** + * 构建有效的通用项目模板保存参数 + * + * @return 项目模板保存参数 + */ + private PmsProjectTemplateSaveReqVO buildProjectTemplateSaveReqVO(String name) { + PmsProjectTemplateSaveReqVO.StatusTemplate todoStatus = new PmsProjectTemplateSaveReqVO.StatusTemplate() + .setCode("task_todo").setName("待处理").setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()) + .setStatusType(PmsWorkItemStatusTypeEnum.PENDING.getType()).setDefaultStatus(true) + .setSort(10).setBoardCode("todo"); + PmsProjectTemplateSaveReqVO.StatusTemplate doneStatus = new PmsProjectTemplateSaveReqVO.StatusTemplate() + .setCode("task_done").setName("已完成").setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()) + .setStatusType(PmsWorkItemStatusTypeEnum.COMPLETED.getType()).setDefaultStatus(false) + .setSort(20).setBoardCode("done"); + PmsProjectTemplateSaveReqVO.BoardTemplate todoBoard = new PmsProjectTemplateSaveReqVO.BoardTemplate() + .setCode("todo").setName("待处理").setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()) + .setSort(10).setStatusCodes(singletonList("task_todo")); + PmsProjectTemplateSaveReqVO.BoardTemplate doneBoard = new PmsProjectTemplateSaveReqVO.BoardTemplate() + .setCode("done").setName("已完成").setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()) + .setSort(20).setStatusCodes(singletonList("task_done")); + return new PmsProjectTemplateSaveReqVO().setName(name).setDescription("通用协作") + .setProjectType(PmsProjectTypeEnum.GENERAL.getType()).setStatus(ENABLE.getStatus()).setSort(10) + .setItemTypes(singletonList(PmsWorkItemTypeEnum.TASK.getType())) + .setStatuses(asList(todoStatus, doneStatus)).setBoards(asList(todoBoard, doneBoard)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchServiceImplTest.java new file mode 100644 index 000000000..af9f16c62 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workbench/PmsWorkbenchServiceImplTest.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.pms.service.pm.workbench; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchCountRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workbench.vo.PmsWorkbenchPageReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.pms.service.pm.workitem.PmsWorkItemService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +/** + * {@link PmsWorkbenchServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsWorkbenchServiceImpl.class) +public class PmsWorkbenchServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsWorkbenchServiceImpl workbenchService; + @MockitoBean + private PmsProjectMemberService projectMemberService; + @MockitoBean + private PmsWorkItemService workItemService; + @MockitoBean + private PmsIterationService iterationService; + + @Test + public void testGetWorkItemPage_filtersOwnerAndCompleted() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = mockActiveProject(userId); + Long iterationId = randomLongId(); + LocalDateTime[] endTime = new LocalDateTime[]{LocalDateTime.now().minusDays(1), LocalDateTime.now()}; + PmsWorkbenchPageReqVO pageReqVO = new PmsWorkbenchPageReqVO().setProjectId(projectId) + .setStatus(PmsWorkItemStatusTypeEnum.PROCESSING.getType()).setPriority(2) + .setIterationId(iterationId).setEndTime(endTime); + when(workItemService.getAssignedWorkItemPage( + pageReqVO, Collections.singletonList(projectId), userId)) + .thenReturn(new PageResult<>(Collections.singletonList( + randomWorkItemDO(PmsWorkItemTypeEnum.TASK.getType(), PmsWorkItemStatusTypeEnum.PROCESSING.getType())), + 1L)); + + // 调用 + PageResult pageResult = workbenchService.getWorkbenchWorkItemPage( + pageReqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + } + + @Test + public void testGetCount_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = mockActiveProject(userId); + PmsWorkbenchPageReqVO pageReqVO = new PmsWorkbenchPageReqVO().setProjectId(projectId) + .setName("登录").setPriority(2); + Map typeCountMap = new HashMap<>(); + typeCountMap.put(PmsWorkItemTypeEnum.REQUIREMENT.getType(), 1L); + typeCountMap.put(PmsWorkItemTypeEnum.DEFECT.getType(), 1L); + when(workItemService.getAssignedWorkItemTypeCountMap(pageReqVO, + Collections.singletonList(projectId), userId)) + .thenReturn(typeCountMap); + when(iterationService.getUncompletedIterationCount(pageReqVO, + Collections.singletonList(projectId), userId)).thenReturn(1L); + + // 调用 + PmsWorkbenchCountRespVO count = workbenchService.getWorkbenchCount(pageReqVO, userId); + + // 断言 + assertEquals(1, count.getRequirementCount()); + assertEquals(1, count.getDefectCount()); + assertEquals(1, count.getIterationCount()); + } + + // ========== 随机对象 ========== + + private Long mockActiveProject(Long userId) { + Long projectId = randomLongId(); + when(projectMemberService.getActiveProjectIdListByUserId(userId)).thenReturn(Collections.singletonList(projectId)); + return projectId; + } + + private PmsWorkItemDO randomWorkItemDO(Integer type, Integer status) { + return new PmsWorkItemDO().setId(randomLongId()).setType(type).setStatus(status); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityServiceImplTest.java new file mode 100644 index 000000000..208d34865 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemActivityServiceImplTest.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemActivityDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemActivityMapper; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.WORK_ITEM_CREATED; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link PmsWorkItemActivityServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsWorkItemActivityServiceImpl.class) +public class PmsWorkItemActivityServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsWorkItemActivityServiceImpl activityService; + + @Resource + private PmsWorkItemActivityMapper activityMapper; + + @MockitoBean + private PmsWorkItemLabelService workItemLabelService; + @MockitoBean + private PmsWorkItemService workItemService; + @MockitoBean + private PmsIterationService iterationService; + @MockitoBean + private AdminUserApi adminUserApi; + + @Test + public void testRecordAndGetActivity_success() { + // 准备参数 + Long projectId = randomLongId(); + Long workItemId = randomLongId(); + Long userId = randomLongId(); + + // 调用 + activityService.createWorkItemActivity(projectId, workItemId, userId, WORK_ITEM_CREATED); + List activities = activityService.getWorkItemActivityList(workItemId); + + // 断言 + assertEquals(1, activities.size()); + PmsWorkItemActivityDO activity = CollUtil.getFirst(activities); + assertEquals(projectId, activity.getProjectId()); + assertEquals(userId, activity.getOperatorUserId()); + assertEquals("创建了工作项", activity.getContent()); + } + + @Test + public void testDeleteWorkItemActivityListByProjectId_success() { + // mock 数据 + Long projectId = randomLongId(); + PmsWorkItemActivityDO activity = new PmsWorkItemActivityDO().setProjectId(projectId) + .setWorkItemId(randomLongId()).setOperatorUserId(randomLongId()).setContent("更新了工作项"); + activityMapper.insert(activity); + + // 调用 + activityService.deleteWorkItemActivityListByProjectId(projectId); + + // 断言 + assertEquals(0, activityService.getWorkItemActivityList(activity.getWorkItemId()).size()); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentServiceImplTest.java new file mode 100644 index 000000000..23345e4fb --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemCommentServiceImplTest.java @@ -0,0 +1,118 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.comment.PmsWorkItemCommentSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemCommentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemCommentMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_COMMENT_ACCESS_DENIED; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_COMMENT_REPLY_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link PmsWorkItemCommentServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsWorkItemCommentServiceImpl.class) +public class PmsWorkItemCommentServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsWorkItemCommentServiceImpl commentService; + + @Resource + private PmsWorkItemCommentMapper commentMapper; + + @MockitoBean + private PmsWorkItemService workItemService; + @MockitoBean + private PmsWorkItemActivityService workItemActivityService; + + @Test + public void testCreateCommentAndReply_success() { + // 准备参数 + Long workItemId = randomLongId(); + Long userId = randomLongId(); + when(workItemService.getWritableWorkItem(eq(workItemId), anyLong())) + .thenReturn(new PmsWorkItemDO().setId(workItemId).setProjectId(randomLongId())); + PmsWorkItemCommentSaveReqVO rootReqVO = new PmsWorkItemCommentSaveReqVO() + .setWorkItemId(workItemId).setContent(" 主评论 "); + + // 调用 + Long rootId = commentService.createWorkItemComment(rootReqVO, userId); + Long replyId = commentService.createWorkItemComment(new PmsWorkItemCommentSaveReqVO() + .setWorkItemId(workItemId).setMainId(rootId).setReplyUserId(userId).setContent("回复"), + randomLongId()); + + // 断言 + assertEquals(" 主评论 ", commentMapper.selectById(rootId).getContent()); + assertEquals(rootId, commentMapper.selectById(replyId).getMainId()); + assertEquals(2, commentService.getWorkItemCommentList(workItemId, userId).size()); + } + + @Test + public void testCreateComment_replyOtherWorkItem() { + // mock 数据 + PmsWorkItemCommentDO mainComment = randomCommentDO(randomLongId(), randomLongId(), 0L); + commentMapper.insert(mainComment); + Long workItemId = randomLongId(); + when(workItemService.getWritableWorkItem(eq(workItemId), anyLong())) + .thenReturn(new PmsWorkItemDO().setId(workItemId).setProjectId(randomLongId())); + + // 调用,并断言异常 + assertServiceException(() -> commentService.createWorkItemComment(new PmsWorkItemCommentSaveReqVO() + .setWorkItemId(workItemId).setMainId(mainComment.getId()).setContent("回复"), + randomLongId()), WORK_ITEM_COMMENT_REPLY_INVALID); + } + + @Test + public void testUpdateComment_notOwner() { + // mock 数据 + PmsWorkItemCommentDO comment = randomCommentDO(randomLongId(), randomLongId(), 0L); + commentMapper.insert(comment); + + // 调用,并断言异常 + assertServiceException(() -> commentService.updateWorkItemComment(new PmsWorkItemCommentSaveReqVO() + .setId(comment.getId()).setWorkItemId(comment.getWorkItemId()).setContent("修改"), + randomLongId()), WORK_ITEM_COMMENT_ACCESS_DENIED); + } + + @Test + public void testDeleteComment_deleteReplies() { + // mock 数据 + Long userId = randomLongId(); + PmsWorkItemCommentDO root = randomCommentDO(randomLongId(), userId, 0L); + commentMapper.insert(root); + PmsWorkItemCommentDO reply = randomCommentDO(root.getWorkItemId(), randomLongId(), root.getId()); + commentMapper.insert(reply); + when(workItemService.getWritableWorkItem(root.getWorkItemId(), userId)) + .thenReturn(new PmsWorkItemDO().setId(root.getWorkItemId()).setProjectId(randomLongId())); + + // 调用 + commentService.deleteWorkItemComment(root.getId(), userId); + + // 断言 + assertNull(commentMapper.selectById(root.getId())); + assertNull(commentMapper.selectById(reply.getId())); + } + + // ========== 随机对象 ========== + + private PmsWorkItemCommentDO randomCommentDO(Long workItemId, Long userId, Long mainId) { + return randomPojo(PmsWorkItemCommentDO.class, comment -> comment.setId(null).setWorkItemId(workItemId) + .setUserId(userId).setMainId(mainId).setContent("评论")); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelServiceImplTest.java new file mode 100644 index 000000000..3e61fbb24 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemLabelServiceImplTest.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label.PmsWorkItemLabelSaveReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemLabelMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import java.util.Arrays; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_LABELS_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link PmsWorkItemLabelServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsWorkItemLabelServiceImpl.class) +public class PmsWorkItemLabelServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsWorkItemLabelServiceImpl labelService; + + @Resource + private PmsWorkItemLabelMapper labelMapper; + + @Test + public void testCreateWorkItemLabel_success() { + // 准备参数 + PmsWorkItemLabelSaveReqVO createReqVO = new PmsWorkItemLabelSaveReqVO() + .setName(" 紧急 ").setColor(" #F56C6C "); + + // 调用 + Long labelId = labelService.createWorkItemLabel(createReqVO); + + // 断言 + PmsWorkItemLabelDO label = labelMapper.selectById(labelId); + assertEquals("紧急", label.getName()); + assertEquals("#F56C6C", label.getColor()); + } + + @Test + public void testUpdateWorkItemLabel_success() { + // mock 数据 + PmsWorkItemLabelDO label = new PmsWorkItemLabelDO().setName("紧急").setColor("#F56C6C"); + labelMapper.insert(label); + // 准备参数 + PmsWorkItemLabelSaveReqVO reqVO = new PmsWorkItemLabelSaveReqVO().setId(label.getId()) + .setName("普通").setColor("#409EFF"); + + // 调用 + labelService.updateWorkItemLabel(reqVO); + + // 断言 + assertEquals("普通", labelMapper.selectById(label.getId()).getName()); + } + + @Test + public void testValidateLabelIds_notExists() { + // 调用,并断言异常 + assertServiceException(() -> labelService.validateWorkItemLabelIds( + Arrays.asList(randomLongId(), randomLongId())), WORK_ITEM_LABELS_INVALID); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemServiceImplTest.java new file mode 100644 index 000000000..0dfd682a8 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemServiceImplTest.java @@ -0,0 +1,1119 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemBoardReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemBoardRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemImportExcelVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemImportRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemIterationUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemNameUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPageReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemPlanningSortReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem.PmsWorkItemSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemCommentDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemLabelDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemMemberDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemBoardMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemCommentMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemMemberMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemStatusMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemUserSortMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemWorkLogMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemDefectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemLifecycleStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_DEFECT_TYPE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_DELETE_STATUS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_PARENT_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_SORT_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_TYPE_INVALID; +import static cn.iocoder.yudao.module.pms.enums.MessageTemplateConstants.WORK_ITEM_ASSIGNED; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.WORK_ITEM_CREATED; +import static cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemActivityContentEnum.WORK_ITEM_NAME_UPDATED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsWorkItemServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import({PmsWorkItemServiceImpl.class, PmsWorkItemUserSortServiceImpl.class, PmsWorkItemStatusServiceImpl.class, + PmsWorkItemBoardServiceImpl.class, + PmsWorkItemCommentServiceImpl.class, PmsWorkItemWorkLogServiceImpl.class}) +public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsWorkItemServiceImpl workItemService; + + @Resource + private PmsWorkItemMapper workItemMapper; + @Resource + private PmsWorkItemStatusMapper workItemStatusMapper; + @Resource + private PmsWorkItemBoardMapper workItemBoardMapper; + @Resource + private PmsWorkItemMemberMapper workItemMemberMapper; + @Resource + private PmsWorkItemCommentMapper workItemCommentMapper; + @Resource + private PmsWorkItemWorkLogMapper workItemWorkLogMapper; + @Resource + private PmsWorkItemUserSortMapper workItemUserSortMapper; + + @MockitoBean + private PmsProjectMemberService projectMemberService; + @MockitoBean + private PmsIterationService iterationService; + @MockitoBean + private PmsWorkItemLabelService workItemLabelService; + @MockitoBean + private PmsWorkItemActivityService workItemActivityService; + @Resource + private PmsWorkItemStatusServiceImpl workItemStatusService; + + @MockitoBean + private NotifyMessageSendApi notifyMessageSendApi; + @MockitoBean + private AdminUserApi adminUserApi; + + @Test + public void testCreateWorkItem_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsProjectDO project = randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType()); + when(projectMemberService.validateProjectWritable(projectId, userId)).thenReturn(project); + workItemStatusService.initProjectWorkItemStatuses(projectId, project.getType()); + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(projectId, PmsWorkItemTypeEnum.TASK.getType()); + reqVO.setAssigneeUserId(randomLongId()).setMemberUserIds(Arrays.asList(randomLongId(), randomLongId())) + .setFileUrls(Arrays.asList("https://example.com/a.png", "https://example.com/b.pdf")); + + // 调用 + Long workItemId = workItemService.createWorkItem(reqVO, userId); + + // 断言 + PmsWorkItemDO workItem = workItemMapper.selectById(workItemId); + assertNotNull(workItem); + assertEquals(reqVO.getName(), workItem.getName()); + assertEquals(1, workItem.getSerialNumber()); + assertEquals(PmsWorkItemStatusTypeEnum.PENDING.getType(), workItem.getStatus()); + assertEquals(reqVO.getFileUrls(), workItem.getFileUrls()); + assertEquals(2, workItemMemberMapper.selectListByWorkItemIds(Collections.singleton(workItemId)).size()); + assertEquals(3, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.TASK.getType()).size()); + verify(projectMemberService).validateProjectMemberList(eq(projectId), anySet()); + ArgumentCaptor captor = ArgumentCaptor + .forClass(NotifySendSingleToUserReqDTO.class); + verify(notifyMessageSendApi).sendSingleMessageToAdmin(captor.capture()); + assertEquals(reqVO.getAssigneeUserId(), captor.getValue().getUserId()); + assertEquals(WORK_ITEM_ASSIGNED, captor.getValue().getTemplateCode()); + assertEquals(project.getName(), captor.getValue().getTemplateParams().get("projectName")); + assertEquals("/pms/pm/project/detail/" + projectId + "?tabs=task", + captor.getValue().getTemplateParams().get("route")); + } + + @Test + public void testCreateWorkItem_withChildrenAndWorkLog() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.AGILE.getType()); + // 准备参数 + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(projectId, PmsWorkItemTypeEnum.TASK.getType()) + .setChildWorkItemNames(Arrays.asList("接口联调", "页面联调")) + .setActualHours(2).setRemainingHours(6); + + // 调用 + Long workItemId = workItemService.createWorkItem(reqVO, userId); + + // 断言 + PmsWorkItemDO parent = workItemMapper.selectById(workItemId); + List children = workItemMapper.selectList(new PmsWorkItemPageReqVO() + .setProjectId(projectId).setParentId(workItemId)); + assertEquals(2, children.size()); + assertTrue(children.stream().allMatch(child -> child.getStatusId().equals(parent.getStatusId()))); + List workLogs = workItemWorkLogMapper.selectListByWorkItemId(workItemId); + assertEquals(1, workLogs.size()); + assertEquals(2, workLogs.get(0).getActualHours()); + assertEquals(6, workLogs.get(0).getRemainingHours()); + } + + @Test + public void testCreateWorkItem_generalProjectTypeInvalid() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.GENERAL.getType())); + // 准备参数 + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(projectId, PmsWorkItemTypeEnum.REQUIREMENT.getType()); + + // 调用,并断言异常 + assertServiceException(() -> workItemService.createWorkItem(reqVO, userId), WORK_ITEM_TYPE_INVALID); + } + + @Test + public void testCreateWorkItem_defectTypeRequired() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(projectId, PmsWorkItemTypeEnum.DEFECT.getType()); + + // 调用,并断言异常 + assertServiceException(() -> workItemService.createWorkItem(reqVO, userId), WORK_ITEM_DEFECT_TYPE_INVALID); + } + + @Test + public void testUpdateWorkItem_parentCycle() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO parent = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(parent); + PmsWorkItemDO child = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2) + .setParentId(parent.getId()); + workItemMapper.insert(child); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(parent); + reqVO.setParentId(child.getId()); + + // 调用,并断言异常 + assertServiceException(() -> workItemService.updateWorkItem(reqVO, userId), WORK_ITEM_PARENT_INVALID); + } + + @Test + public void testUpdateWorkItem_clearOptionalFields() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO parent = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(parent); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2) + .setDescription("旧描述").setAssigneeUserId(randomLongId()).setParentId(parent.getId()) + .setStartTime(LocalDateTime.now()).setEndTime(LocalDateTime.now().plusDays(1)) + .setEstimatedHours(8).setFileUrls(Collections.singletonList("https://example.com/a.png")); + workItemMapper.insert(workItem); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(workItem); + reqVO.setDescription(null).setAssigneeUserId(null).setParentId(null) + .setStartTime(null).setEndTime(null).setEstimatedHours(null).setFileUrls(null); + + // 调用 + workItemService.updateWorkItem(reqVO, userId); + + // 断言 + PmsWorkItemDO updatedWorkItem = workItemMapper.selectById(workItem.getId()); + assertNull(updatedWorkItem.getDescription()); + assertNull(updatedWorkItem.getAssigneeUserId()); + assertNull(updatedWorkItem.getParentId()); + assertNull(updatedWorkItem.getStartTime()); + assertNull(updatedWorkItem.getEndTime()); + assertNull(updatedWorkItem.getEstimatedHours()); + assertNull(updatedWorkItem.getFileUrls()); + } + + @Test + public void testUpdateWorkItem_assigneeChangedNotify() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + Long oldAssigneeUserId = randomLongId(); + Long newAssigneeUserId = randomLongId(); + PmsProjectDO project = randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType()); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1) + .setAssigneeUserId(oldAssigneeUserId); + workItemMapper.insert(workItem); + when(projectMemberService.validateProjectWritable(projectId, userId)).thenReturn(project); + // 准备参数 + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(workItem).setAssigneeUserId(newAssigneeUserId); + + // 调用 + workItemService.updateWorkItem(reqVO, userId); + + // 断言 + ArgumentCaptor captor = ArgumentCaptor + .forClass(NotifySendSingleToUserReqDTO.class); + verify(notifyMessageSendApi).sendSingleMessageToAdmin(captor.capture()); + assertEquals(newAssigneeUserId, captor.getValue().getUserId()); + assertEquals(reqVO.getName(), captor.getValue().getTemplateParams().get("workItemName")); + } + + @Test + public void testUpdateWorkItemName_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(workItem); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + PmsWorkItemNameUpdateReqVO reqVO = new PmsWorkItemNameUpdateReqVO().setId(workItem.getId()) + .setName(" 子任务新名称 "); + + // 调用 + workItemService.updateWorkItemName(reqVO, userId); + + // 断言 + assertEquals("子任务新名称", workItemMapper.selectById(workItem.getId()).getName()); + verify(workItemActivityService).createWorkItemActivity( + projectId, workItem.getId(), userId, WORK_ITEM_NAME_UPDATED, + workItem.getName(), "子任务新名称"); + } + + @Test + public void testUpdateWorkItem_createFieldActivitiesDelegates() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + Long oldAssigneeUserId = randomLongId(); + Long newAssigneeUserId = randomLongId(); + Long oldMemberUserId = randomLongId(); + Long newMemberUserId = randomLongId(); + Long oldLabelId = randomLongId(); + Long newLabelId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1) + .setDescription("旧描述").setAssigneeUserId(oldAssigneeUserId) + .setFileUrls(Collections.singletonList("https://example.com/old.png")) + .setLabelIds(Collections.singletonList(oldLabelId)); + workItemMapper.insert(workItem); + workItemMemberMapper.insert(new PmsWorkItemMemberDO().setProjectId(projectId) + .setWorkItemId(workItem.getId()).setUserId(oldMemberUserId)); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + Map userMap = new HashMap<>(); + userMap.put(oldAssigneeUserId, new AdminUserRespDTO().setId(oldAssigneeUserId).setNickname("旧负责人")); + userMap.put(newAssigneeUserId, new AdminUserRespDTO().setId(newAssigneeUserId).setNickname("新负责人")); + userMap.put(oldMemberUserId, new AdminUserRespDTO().setId(oldMemberUserId).setNickname("旧参与人")); + userMap.put(newMemberUserId, new AdminUserRespDTO().setId(newMemberUserId).setNickname("新参与人")); + when(adminUserApi.getUserMap(anySet())).thenReturn(userMap); + Map labelMap = new HashMap<>(); + labelMap.put(oldLabelId, new PmsWorkItemLabelDO().setId(oldLabelId).setName("旧标签")); + labelMap.put(newLabelId, new PmsWorkItemLabelDO().setId(newLabelId).setName("新标签")); + when(workItemLabelService.getWorkItemLabelMap(anySet())).thenReturn(labelMap); + // 准备参数 + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(workItem).setDescription("新描述") + .setPriority(PmsWorkItemPriorityEnum.HIGH.getPriority()).setAssigneeUserId(newAssigneeUserId) + .setMemberUserIds(Collections.singletonList(newMemberUserId)) + .setFileUrls(Collections.singletonList("https://example.com/new.pdf")) + .setLabelIds(Collections.singletonList(newLabelId)); + + // 调用 + workItemService.updateWorkItem(reqVO, userId); + + // 断言 + verify(workItemActivityService).createWorkItemUpdateActivities(eq(workItem), any(PmsWorkItemDO.class), + anyCollection(), anyCollection(), eq(userId)); + } + + @Test + public void testUpdateWorkItemStatus_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO pendingStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(pendingStatus); + PmsWorkItemStatusDO completedStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.COMPLETED.getType(), false, 2); + workItemStatusMapper.insert(completedStatus); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), pendingStatus, 1); + workItemMapper.insert(workItem); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemStatusUpdateReqVO reqVO = new PmsWorkItemStatusUpdateReqVO().setId(workItem.getId()) + .setStatusId(completedStatus.getId()); + + // 调用 + workItemService.updateWorkItemStatus(reqVO, userId); + + // 断言 + PmsWorkItemDO updatedWorkItem = workItemMapper.selectById(workItem.getId()); + assertEquals(completedStatus.getId(), updatedWorkItem.getStatusId()); + assertEquals(PmsWorkItemStatusTypeEnum.COMPLETED.getType(), updatedWorkItem.getStatus()); + assertEquals(0, updatedWorkItem.getProgress()); + } + + @Test + public void testUpdateWorkItemIteration_planAndUnplan() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + Long iterationId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(workItem); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + when(iterationService.getIteration(iterationId, userId)) + .thenReturn(new PmsIterationDO().setId(iterationId).setProjectId(projectId)); + + // 调用:规划到迭代 + workItemService.updateWorkItemIteration(new PmsWorkItemIterationUpdateReqVO() + .setId(workItem.getId()).setIterationId(iterationId), userId); + + // 断言 + assertEquals(iterationId, workItemMapper.selectById(workItem.getId()).getIterationId()); + + // 调用:移回待规划,并断言 + workItemService.updateWorkItemIteration(new PmsWorkItemIterationUpdateReqVO() + .setId(workItem.getId()), userId); + assertNull(workItemMapper.selectById(workItem.getId()).getIterationId()); + } + + @Test + public void testCreateWorkItem_emptyMembersDefaultsToCreator() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.AGILE.getType()); + // 准备参数 + PmsWorkItemSaveReqVO reqVO = randomSaveReqVO(projectId, PmsWorkItemTypeEnum.TASK.getType()); + + // 调用 + Long workItemId = workItemService.createWorkItem(reqVO, userId); + + // 断言 + List members = workItemMemberMapper + .selectListByWorkItemIds(Collections.singleton(workItemId)); + assertEquals(1, members.size()); + assertEquals(userId, CollUtil.getFirst(members).getUserId()); + } + + @Test + public void testUpdateWorkItemSort_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO first = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(first); + PmsWorkItemDO second = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2); + workItemMapper.insert(second); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemSortReqVO reqVO = new PmsWorkItemSortReqVO().setStatusId(status.getId()) + .setWorkItemIds(Arrays.asList(second.getId(), first.getId())); + + // 调用 + workItemService.updateWorkItemSort(reqVO, userId); + + // 断言 + assertEquals(1, workItemMapper.selectById(second.getId()).getSort()); + assertEquals(2, workItemMapper.selectById(first.getId()).getSort()); + } + + @Test + public void testUpdateWorkItemSort_duplicateId() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(workItem); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemSortReqVO reqVO = new PmsWorkItemSortReqVO().setStatusId(status.getId()) + .setWorkItemIds(Arrays.asList(workItem.getId(), workItem.getId())); + + // 调用,并断言异常 + assertServiceException(() -> workItemService.updateWorkItemSort(reqVO, userId), WORK_ITEM_SORT_INVALID); + } + + @Test + public void testUpdateWorkItemPlanningSort_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO taskStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(taskStatus); + PmsWorkItemStatusDO defectStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.DEFECT.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(defectStatus); + PmsWorkItemDO first = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), taskStatus, 1); + workItemMapper.insert(first); + PmsWorkItemDO second = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.DEFECT.getType(), defectStatus, 2); + workItemMapper.insert(second); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemPlanningSortReqVO reqVO = new PmsWorkItemPlanningSortReqVO().setProjectId(projectId) + .setWorkItemIds(Arrays.asList(second.getId(), first.getId())); + + // 调用 + workItemService.updateWorkItemPlanningSort(reqVO, userId); + workItemService.updateWorkItemPlanningSort(reqVO, userId); + + // 断言:公共顺序不变,个人顺序按当前用户单独保存 + assertEquals(1, workItemMapper.selectById(first.getId()).getSort()); + assertEquals(2, workItemMapper.selectById(second.getId()).getSort()); + List userSorts = workItemUserSortMapper + .selectListByProjectIdAndUserId(projectId, userId); + assertEquals(Arrays.asList(second.getId(), first.getId()), + convertList(userSorts, PmsWorkItemUserSortDO::getWorkItemId)); + + // 断言:当前用户和其他用户读取到的 Backlog 顺序互不影响 + Long otherUserId = randomLongId(); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + when(projectMemberService.validateProjectReadable(projectId, otherUserId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + PmsWorkItemPageReqVO pageReqVO = new PmsWorkItemPageReqVO().setProjectId(projectId) + .setPlanningOnly(true).setUnplannedOnly(true); + assertEquals(Arrays.asList(second.getId(), first.getId()), + convertList(workItemService.getWorkItemPage(pageReqVO, userId).getList(), PmsWorkItemDO::getId)); + assertEquals(Arrays.asList(first.getId(), second.getId()), + convertList(workItemService.getWorkItemPage(pageReqVO, otherUserId).getList(), PmsWorkItemDO::getId)); + } + + @Test + public void testDeleteWorkItem_requirementCleanup() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO requirementStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.REQUIREMENT.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(requirementStatus); + PmsWorkItemStatusDO defectStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.DEFECT.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(defectStatus); + PmsWorkItemDO requirement = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.REQUIREMENT.getType(), + requirementStatus, 1).setLifecycleStatus(PmsWorkItemLifecycleStatusEnum.RECYCLED.getStatus()); + workItemMapper.insert(requirement); + PmsWorkItemDO defect = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.DEFECT.getType(), defectStatus, 2) + .setRelatedRequirementId(requirement.getId()).setDefectType(PmsWorkItemDefectTypeEnum.CODE.getType()); + workItemMapper.insert(defect); + workItemMemberMapper.insert(new PmsWorkItemMemberDO().setProjectId(projectId) + .setWorkItemId(requirement.getId()).setUserId(randomLongId())); + PmsWorkItemWorkLogDO workLog = new PmsWorkItemWorkLogDO().setProjectId(projectId) + .setWorkItemId(requirement.getId()).setActualHours(2); + workItemWorkLogMapper.insert(workLog); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + + // 调用 + workItemService.deleteWorkItem(requirement.getId(), userId); + + // 断言 + assertNull(workItemMapper.selectById(requirement.getId())); + assertNull(workItemMapper.selectById(defect.getId()).getRelatedRequirementId()); + assertEquals(0, workItemMemberMapper.selectListByWorkItemIds( + Collections.singleton(requirement.getId())).size()); + assertNull(workItemWorkLogMapper.selectById(workLog.getId())); + } + + @Test + public void testArchiveAndRestoreWorkItem_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(workItem); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + + // 调用:归档 + workItemService.archiveWorkItem(workItem.getId(), userId); + + // 断言 + PmsWorkItemDO archivedWorkItem = workItemMapper.selectById(workItem.getId()); + assertEquals(PmsWorkItemLifecycleStatusEnum.ARCHIVED.getStatus(), archivedWorkItem.getLifecycleStatus()); + assertNotNull(archivedWorkItem.getArchiveTime()); + + // 调用:恢复,并断言 + workItemService.restoreWorkItem(workItem.getId(), userId); + PmsWorkItemDO restoredWorkItem = workItemMapper.selectById(workItem.getId()); + assertEquals(PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus(), restoredWorkItem.getLifecycleStatus()); + assertNull(restoredWorkItem.getArchiveTime()); + assertNull(restoredWorkItem.getRecycleTime()); + } + + @Test + public void testRecycleAndDeleteWorkItem_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(workItem); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + + // 调用:移入回收站 + workItemService.recycleWorkItem(workItem.getId(), userId); + + // 断言 + PmsWorkItemDO recycledWorkItem = workItemMapper.selectById(workItem.getId()); + assertEquals(PmsWorkItemLifecycleStatusEnum.RECYCLED.getStatus(), recycledWorkItem.getLifecycleStatus()); + assertNotNull(recycledWorkItem.getRecycleTime()); + + // 调用:彻底删除,并断言 + workItemService.deleteWorkItem(workItem.getId(), userId); + assertNull(workItemMapper.selectById(workItem.getId())); + } + + @Test + public void testDeleteWorkItem_activeStatusInvalid() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(workItem); + + // 调用,并断言异常 + assertServiceException(() -> workItemService.deleteWorkItem(workItem.getId(), userId), + WORK_ITEM_DELETE_STATUS_INVALID); + } + + @Test + public void testDeleteWorkItemListByProjectId_cleanupComments() { + // mock 数据 + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO workItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(workItem); + PmsWorkItemCommentDO comment = new PmsWorkItemCommentDO().setWorkItemId(workItem.getId()) + .setUserId(randomLongId()).setContent("项目删除时清理评论"); + workItemCommentMapper.insert(comment); + + // 调用 + workItemService.deleteWorkItemListByProjectId(projectId); + + // 断言 + assertNull(workItemMapper.selectById(workItem.getId())); + assertNull(workItemCommentMapper.selectById(comment.getId())); + } + + @Test + public void testGetWorkItemBoard_keepsEmptyStatus() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO pendingStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1).setBoardName("状态1"); + workItemStatusMapper.insert(pendingStatus); + workItemStatusMapper.insert(randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PROCESSING.getType(), false, 2).setBoardName("状态2")); + workItemBoardMapper.insert(new PmsWorkItemBoardDO().setProjectId(projectId) + .setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()).setName("状态1").setSort(1)); + workItemBoardMapper.insert(new PmsWorkItemBoardDO().setProjectId(projectId) + .setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()).setName("状态2").setSort(2)); + PmsWorkItemDO parent = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), pendingStatus, 1); + workItemMapper.insert(parent); + PmsWorkItemDO child = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), pendingStatus, 2) + .setParentId(parent.getId()); + workItemMapper.insert(child); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemBoardReqVO reqVO = new PmsWorkItemBoardReqVO(); + reqVO.setProjectId(projectId); + reqVO.setType(PmsWorkItemTypeEnum.TASK.getType()); + + // 调用 + List board = workItemService.getWorkItemBoard(reqVO, userId); + + // 断言 + assertEquals(2, board.size()); + assertEquals(1, CollUtil.getFirst(board).getItems().size()); + assertEquals(parent.getId(), CollUtil.getFirst(CollUtil.getFirst(board).getItems()).getId()); + PmsWorkItemBoardRespVO emptyBoard = CollUtil.findOne(board, column -> "状态2".equals(column.getName())); + assertNotNull(emptyBoard); + assertEquals(0, emptyBoard.getItems().size()); + } + + @Test + public void testGetWorkItemBoard_groupsStatusesByBoardName() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO developingStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PROCESSING.getType(), false, 1).setBoardName("处理中"); + workItemStatusMapper.insert(developingStatus); + PmsWorkItemStatusDO testingStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PROCESSING.getType(), false, 2).setBoardName("处理中"); + workItemStatusMapper.insert(testingStatus); + workItemMapper.insert(randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), developingStatus, 1)); + workItemMapper.insert(randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), testingStatus, 2)); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + workItemBoardMapper.insert(new PmsWorkItemBoardDO().setProjectId(projectId) + .setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()).setName("处理中").setSort(1)); + + // 调用 + PmsWorkItemBoardReqVO reqVO = new PmsWorkItemBoardReqVO(); + reqVO.setProjectId(projectId); + reqVO.setType(PmsWorkItemTypeEnum.TASK.getType()); + List board = workItemService.getWorkItemBoard(reqVO, userId); + + // 断言 + assertEquals(1, board.size()); + PmsWorkItemBoardRespVO processingBoard = CollUtil.findOne(board, + column -> "处理中".equals(column.getName())); + assertNotNull(processingBoard); + assertEquals(2, processingBoard.getStatuses().size()); + assertEquals(2, processingBoard.getItems().size()); + } + + @Test + public void testGetWorkItemPage_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO matched = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1) + .setName("登录任务"); + workItemMapper.insert(matched); + workItemMapper.insert(randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2) + .setName("其他任务")); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemPageReqVO reqVO = new PmsWorkItemPageReqVO().setProjectId(projectId) + .setType(PmsWorkItemTypeEnum.TASK.getType()).setName("登录"); + + // 调用 + PageResult pageResult = workItemService.getWorkItemPage(reqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(matched.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetWorkItemPage_orderByPriorityAndSerialNumber() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO mediumWorkItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1) + .setPriority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()); + workItemMapper.insert(mediumWorkItem); + PmsWorkItemDO highSecondWorkItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 3) + .setPriority(PmsWorkItemPriorityEnum.HIGH.getPriority()); + workItemMapper.insert(highSecondWorkItem); + PmsWorkItemDO highFirstWorkItem = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2) + .setPriority(PmsWorkItemPriorityEnum.HIGH.getPriority()); + workItemMapper.insert(highFirstWorkItem); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + + // 调用 + PageResult pageResult = workItemService.getWorkItemPage( + new PmsWorkItemPageReqVO().setProjectId(projectId).setRootOnly(true), userId); + + // 断言 + assertEquals(Arrays.asList(highFirstWorkItem.getId(), highSecondWorkItem.getId(), mediumWorkItem.getId()), + convertList(pageResult.getList(), PmsWorkItemDO::getId)); + } + + @Test + public void testGetWorkItemPage_searchSerialNumber() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + workItemMapper.insert(randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1) + .setName("其他事项")); + PmsWorkItemDO matched = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2); + workItemMapper.insert(matched); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemPageReqVO reqVO = new PmsWorkItemPageReqVO().setProjectId(projectId).setName("#2"); + + // 调用 + PageResult pageResult = workItemService.getWorkItemPage(reqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(matched.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetWorkItemPage_rootOnly() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO parent = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(parent); + PmsWorkItemDO child = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2) + .setParentId(parent.getId()); + workItemMapper.insert(child); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemPageReqVO reqVO = new PmsWorkItemPageReqVO().setProjectId(projectId).setRootOnly(true); + + // 调用 + PageResult pageResult = workItemService.getWorkItemPage(reqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(parent.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetProjectWorkItemStatusCountMap_success() { + // mock 数据 + Long firstProjectId = randomLongId(); + Long secondProjectId = randomLongId(); + PmsWorkItemStatusDO pendingStatus = randomStatusDO(firstProjectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(pendingStatus); + PmsWorkItemStatusDO completedStatus = randomStatusDO(firstProjectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.COMPLETED.getType(), false, 2); + workItemStatusMapper.insert(completedStatus); + workItemMapper.insert(randomWorkItemDO(firstProjectId, PmsWorkItemTypeEnum.TASK.getType(), pendingStatus, 1)); + workItemMapper.insert(randomWorkItemDO(firstProjectId, PmsWorkItemTypeEnum.TASK.getType(), pendingStatus, 2)); + workItemMapper.insert(randomWorkItemDO(firstProjectId, PmsWorkItemTypeEnum.TASK.getType(), completedStatus, 3)); + PmsWorkItemDO archived = randomWorkItemDO(firstProjectId, PmsWorkItemTypeEnum.TASK.getType(), + completedStatus, 4).setLifecycleStatus(PmsWorkItemLifecycleStatusEnum.ARCHIVED.getStatus()); + workItemMapper.insert(archived); + PmsWorkItemStatusDO processingStatus = randomStatusDO(secondProjectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PROCESSING.getType(), true, 1); + workItemStatusMapper.insert(processingStatus); + workItemMapper.insert(randomWorkItemDO(secondProjectId, PmsWorkItemTypeEnum.TASK.getType(), + processingStatus, 1)); + + // 调用 + Map> countMap = workItemService.getProjectWorkItemStatusCountMap( + Arrays.asList(firstProjectId, secondProjectId)); + + // 断言 + assertEquals(2L, countMap.get(firstProjectId).get(PmsWorkItemStatusTypeEnum.PENDING.getType())); + assertEquals(1L, countMap.get(firstProjectId).get(PmsWorkItemStatusTypeEnum.COMPLETED.getType())); + assertEquals(1L, countMap.get(secondProjectId).get(PmsWorkItemStatusTypeEnum.PROCESSING.getType())); + } + + @Test + public void testGetWorkItemPage_filterPriorityAndParentId() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO parent = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(parent); + PmsWorkItemDO matched = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2) + .setParentId(parent.getId()).setPriority(PmsWorkItemPriorityEnum.HIGH.getPriority()); + workItemMapper.insert(matched); + PmsWorkItemDO other = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 3) + .setPriority(PmsWorkItemPriorityEnum.HIGH.getPriority()); + workItemMapper.insert(other); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemPageReqVO reqVO = new PmsWorkItemPageReqVO().setProjectId(projectId) + .setPriority(PmsWorkItemPriorityEnum.HIGH.getPriority()).setParentId(parent.getId()); + + // 调用 + PageResult pageResult = workItemService.getWorkItemPage(reqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(matched.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testBuildQueryWrapper_filterAnyLabel() { + // 准备参数 + PmsWorkItemPageReqVO reqVO = new PmsWorkItemPageReqVO().setProjectId(randomLongId()) + .setLabelIds(Arrays.asList(randomLongId(), randomLongId())); + + // 调用 + String sqlSegment = PmsWorkItemMapper.buildQueryWrapper(reqVO).getSqlSegment(); + + // 断言 + assertTrue(sqlSegment.contains("JSON_CONTAINS(label_ids, JSON_ARRAY(")); + assertTrue(sqlSegment.contains(" OR ")); + } + + @Test + public void testBuildQueryWrapper_combinationFilters() { + // 准备参数 + PmsWorkItemPageReqVO reqVO = new PmsWorkItemPageReqVO().setProjectId(randomLongId()) + .setTypes(Arrays.asList(PmsWorkItemTypeEnum.REQUIREMENT.getType(), + PmsWorkItemTypeEnum.TASK.getType())) + .setStatuses(Arrays.asList(PmsWorkItemStatusTypeEnum.PENDING.getType(), + PmsWorkItemStatusTypeEnum.PROCESSING.getType())) + .setPriorities(Arrays.asList(PmsWorkItemPriorityEnum.MEDIUM.getPriority(), + PmsWorkItemPriorityEnum.HIGH.getPriority())) + .setIterationIds(Arrays.asList(randomLongId(), randomLongId())) + .setExcludedIterationIds(Collections.singletonList(randomLongId())) + .setAssigneeUserIds(Arrays.asList(randomLongId(), randomLongId())); + + // 调用 + String sqlSegment = PmsWorkItemMapper.buildQueryWrapper(reqVO).getSqlSegment(); + + // 断言 + assertTrue(sqlSegment.contains("type IN")); + assertTrue(sqlSegment.contains("status IN")); + assertTrue(sqlSegment.contains("priority IN")); + assertTrue(sqlSegment.contains("iteration_id IN")); + assertTrue(sqlSegment.contains("iteration_id NOT IN")); + assertTrue(sqlSegment.contains("iteration_id IS NULL")); + assertTrue(sqlSegment.contains("assignee_user_id IN")); + } + + @Test + public void testGetWorkItemPage_filterLifecycleStatus() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + workItemMapper.insert(randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1)); + PmsWorkItemDO archived = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2) + .setLifecycleStatus(PmsWorkItemLifecycleStatusEnum.ARCHIVED.getStatus()); + workItemMapper.insert(archived); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemPageReqVO reqVO = new PmsWorkItemPageReqVO().setProjectId(projectId) + .setLifecycleStatus(PmsWorkItemLifecycleStatusEnum.ARCHIVED.getStatus()); + + // 调用 + PageResult pageResult = workItemService.getWorkItemPage(reqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(archived.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testGetWorkItemPage_unplannedOnly() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), + PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); + workItemStatusMapper.insert(status); + PmsWorkItemDO unplanned = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 1); + workItemMapper.insert(unplanned); + PmsWorkItemDO planned = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), status, 2) + .setIterationId(randomLongId()); + workItemMapper.insert(planned); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + // 准备参数 + PmsWorkItemPageReqVO reqVO = new PmsWorkItemPageReqVO().setProjectId(projectId) + .setUnplannedOnly(true); + + // 调用 + PageResult pageResult = workItemService.getWorkItemPage(reqVO, userId); + + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(unplanned.getId(), CollUtil.getFirst(pageResult.getList()).getId()); + } + + @Test + public void testImportWorkItemList_partialSuccess() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.AGILE.getType()); + // 准备参数 + PmsWorkItemImportExcelVO valid = PmsWorkItemImportExcelVO.builder().name("导入任务") + .priority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()).progress(0).build(); + PmsWorkItemImportExcelVO invalid = PmsWorkItemImportExcelVO.builder() + .priority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()).progress(0).build(); + + // 调用 + PmsWorkItemImportRespVO importResult = workItemService.importWorkItemList(projectId, + PmsWorkItemTypeEnum.TASK.getType(), Arrays.asList(valid, invalid), userId); + + // 断言 + assertEquals(1, importResult.getSuccessCount()); + assertEquals(1, importResult.getFailureReasons().size()); + assertNotNull(importResult.getFailureReasons().get(3)); + assertEquals(1, workItemMapper.selectListByProjectId(projectId).size()); + } + + @Test + public void testImportWorkItemList_failureReason() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.AGILE.getType()); + doThrow(new IllegalStateException("动态记录失败")).when(workItemActivityService) + .createWorkItemActivity(anyLong(), anyLong(), anyLong(), + eq(WORK_ITEM_CREATED)); + // 准备参数 + PmsWorkItemImportExcelVO importItem = PmsWorkItemImportExcelVO.builder().name("导入任务") + .priority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()).progress(0).build(); + + // 调用 + PmsWorkItemImportRespVO importResult = workItemService.importWorkItemList(projectId, + PmsWorkItemTypeEnum.TASK.getType(), Collections.singletonList(importItem), userId); + + // 断言 + assertEquals(0, importResult.getSuccessCount()); + assertEquals("动态记录失败", importResult.getFailureReasons().get(2)); + assertEquals(1, workItemMapper.selectListByProjectId(projectId).size()); + assertEquals(3, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.TASK.getType()).size()); + } + + // ========== 随机对象 ========== + + private static PmsWorkItemSaveReqVO randomSaveReqVO(Long projectId, Integer type) { + LocalDateTime startTime = LocalDateTime.now().plusDays(1); + return randomPojo(PmsWorkItemSaveReqVO.class, reqVO -> reqVO.setId(null).setProjectId(projectId) + .setType(type).setName("登录任务").setPriority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()) + .setProgress(0).setDescription(null).setAssigneeUserId(null).setMemberUserIds(null) + .setIterationId(null).setParentId(null).setRelatedRequirementId(null).setDefectType(null) + .setStartTime(startTime).setEndTime(startTime.plusDays(2)).setEstimatedHours(null) + .setFileUrls(null).setLabelIds(null).setChildWorkItemNames(null) + .setActualHours(null).setRemainingHours(null)); + } + + private static PmsWorkItemSaveReqVO randomSaveReqVO(PmsWorkItemDO workItem) { + LocalDateTime startTime = LocalDateTime.now().plusDays(1); + return randomPojo(PmsWorkItemSaveReqVO.class, reqVO -> reqVO.setId(workItem.getId()) + .setProjectId(workItem.getProjectId()).setType(workItem.getType()).setName(workItem.getName()) + .setPriority(workItem.getPriority()).setProgress(workItem.getProgress()) + .setDescription(null).setAssigneeUserId(null).setMemberUserIds(null).setIterationId(null) + .setParentId(null).setRelatedRequirementId(null).setDefectType(null) + .setStartTime(startTime).setEndTime(startTime.plusDays(2)).setEstimatedHours(null) + .setFileUrls(null).setLabelIds(null).setChildWorkItemNames(null) + .setActualHours(null).setRemainingHours(null)); + } + + private static PmsProjectDO randomProjectDO(Long id, Integer type) { + return randomPojo(PmsProjectDO.class, project -> project.setId(id).setName("测试项目") + .setStatus(PmsProjectStatusEnum.ACTIVE.getStatus()).setType(type).setLevel(3).setOpenStatus(false)); + } + + private static PmsWorkItemStatusDO randomStatusDO(Long projectId, Integer workItemType, Integer statusType, + boolean defaultStatus, int sort) { + return randomPojo(PmsWorkItemStatusDO.class, status -> status.setId(null).setProjectId(projectId) + .setWorkItemType(workItemType).setName("状态" + sort).setStatusType(statusType) + .setBoardName(null).setSystemCode(null).setDefaultStatus(defaultStatus).setSort(sort)); + } + + private static PmsWorkItemDO randomWorkItemDO(Long projectId, Integer type, PmsWorkItemStatusDO status, + int serialNumber) { + return randomPojo(PmsWorkItemDO.class, workItem -> workItem.setId(null).setProjectId(projectId).setType(type) + .setSerialNumber(serialNumber).setName("工作项" + serialNumber) + .setPriority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()).setStatusId(status.getId()) + .setStatus(status.getStatusType()).setProgress(0).setSort(serialNumber) + .setLifecycleStatus(PmsWorkItemLifecycleStatusEnum.ACTIVE.getStatus()) + .setDescription(null).setAssigneeUserId(null).setArchiveTime(null).setRecycleTime(null) + .setIterationId(null).setParentId(null).setRelatedRequirementId(null).setDefectType(null) + .setStartTime(null).setEndTime(null).setEstimatedHours(null).setFileUrls(null).setLabelIds(null)); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusServiceImplTest.java new file mode 100644 index 000000000..418da46fa --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemStatusServiceImplTest.java @@ -0,0 +1,426 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemBoardConfigSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusConfigUpdateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusCreateReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusDeleteReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.status.PmsWorkItemStatusSortReqVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemBoardDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemBoardMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemMapper; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemStatusMapper; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; +import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_DEFAULT_CANNOT_DELETE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_INVALID; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_STATUS_TRANSFER_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * {@link PmsWorkItemStatusServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import({PmsWorkItemStatusServiceImpl.class, PmsWorkItemBoardServiceImpl.class, PmsWorkItemServiceImpl.class}) +public class PmsWorkItemStatusServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsWorkItemStatusServiceImpl workItemStatusService; + + @Resource + private PmsWorkItemStatusMapper workItemStatusMapper; + @Resource + private PmsWorkItemBoardMapper workItemBoardMapper; + @Resource + private PmsWorkItemMapper workItemMapper; + + @MockitoBean + private PmsProjectMemberService projectMemberService; + @MockitoBean + private PmsIterationService iterationService; + @MockitoBean + private PmsWorkItemCommentService workItemCommentService; + @MockitoBean + private PmsWorkItemWorkLogService workItemWorkLogService; + @MockitoBean + private PmsWorkItemUserSortService workItemUserSortService; + @MockitoBean + private PmsWorkItemLabelService workItemLabelService; + @MockitoBean + private PmsWorkItemActivityService workItemActivityService; + @MockitoBean + private NotifyMessageSendApi notifyMessageSendApi; + @MockitoBean + private AdminUserApi adminUserApi; + + @Test + public void testInitProjectWorkItemStatuses_success() { + // 准备参数 + Long projectId = randomLongId(); + + // 调用 + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + List statuses = workItemStatusService.getWorkItemStatusList(projectId, + PmsWorkItemTypeEnum.TASK.getType()); + + // 断言 + assertEquals(3, statuses.size()); + assertEquals(PmsWorkItemStatusTypeEnum.PENDING.getType(), CollUtil.getFirst(statuses).getStatusType()); + assertEquals(PmsWorkItemStatusTypeEnum.PENDING.name(), CollUtil.getFirst(statuses).getSystemCode()); + assertEquals(PmsWorkItemStatusTypeEnum.PENDING.getName(), CollUtil.getFirst(statuses).getBoardName()); + assertTrue(CollUtil.getFirst(statuses).getDefaultStatus()); + assertFalse(statuses.get(1).getDefaultStatus()); + assertEquals(3, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.TASK.getType()).size()); + } + + @Test + public void testGetWorkItemStatusList_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + + // 调用 + List statuses = workItemStatusService.getWorkItemStatusList(projectId, + PmsWorkItemTypeEnum.TASK.getType(), userId); + + // 断言 + assertEquals(3, statuses.size()); + assertEquals(PmsWorkItemStatusTypeEnum.PENDING.getType(), CollUtil.getFirst(statuses).getStatusType()); + } + + @Test + public void testGetWorkItemBoardList_readOnly() { + // 准备参数 + Long projectId = randomLongId(); + + // 调用 + List boards = workItemStatusService.getWorkItemBoardList(projectId, + PmsWorkItemTypeEnum.TASK.getType()); + + // 断言:读取看板配置不会补建任何数据 + assertTrue(CollUtil.isEmpty(boards)); + assertTrue(CollUtil.isEmpty(workItemBoardMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.TASK.getType()))); + } + + @Test + public void testGetWorkItemStatus_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + PmsWorkItemStatusDO status = new PmsWorkItemStatusDO().setProjectId(projectId) + .setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()).setName("开发中") + .setStatusType(PmsWorkItemStatusTypeEnum.PROCESSING.getType()) + .setBoardName("进行中").setDefaultStatus(false).setSort(2); + workItemStatusMapper.insert(status); + when(projectMemberService.validateProjectReadable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + + // 调用 + PmsWorkItemStatusDO result = workItemStatusService.getWorkItemStatus(status.getId(), userId); + + // 断言 + assertEquals(status.getId(), result.getId()); + } + + @Test + public void testInitProjectWorkItemStatuses_generalOnlyTask() { + // 准备参数 + Long projectId = randomLongId(); + + // 调用 + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + + // 断言 + assertEquals(3, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.TASK.getType()).size()); + assertEquals(3, workItemBoardMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.TASK.getType()).size()); + assertEquals(0, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.REQUIREMENT.getType()).size()); + assertEquals(0, workItemBoardMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.REQUIREMENT.getType()).size()); + } + + @Test + public void testInitProjectWorkItemStatuses_agileAllTypes() { + // 准备参数 + Long projectId = randomLongId(); + + // 调用 + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.AGILE.getType()); + + // 断言 + assertEquals(3, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.TASK.getType()).size()); + assertEquals(3, workItemBoardMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.TASK.getType()).size()); + assertEquals(3, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.REQUIREMENT.getType()).size()); + assertEquals(3, workItemBoardMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.REQUIREMENT.getType()).size()); + assertEquals(3, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.DEFECT.getType()).size()); + assertEquals(3, workItemBoardMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.DEFECT.getType()).size()); + } + + @Test + public void testInitProjectWorkItemStatuses_idempotent() { + // 准备参数 + Long projectId = randomLongId(); + + // 调用 + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.AGILE.getType()); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.AGILE.getType()); + + // 断言 + assertEquals(3, workItemStatusMapper.selectListByProjectIdAndWorkItemType(projectId, + PmsWorkItemTypeEnum.REQUIREMENT.getType()).size()); + } + + @Test + public void testGetWorkItemStatusList_doesNotRecreateDeletedSystemStatus() { + // 准备参数 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + List statuses = workItemStatusService.getWorkItemStatusList(projectId, + PmsWorkItemTypeEnum.TASK.getType()); + PmsWorkItemStatusDO processingStatus = statuses.get(1); + + // 调用:删除进行中系统状态后重新加载状态列表 + workItemStatusService.deleteWorkItemStatus(new PmsWorkItemStatusDeleteReqVO() + .setId(processingStatus.getId()), userId); + List recreatedStatuses = workItemStatusService + .getWorkItemStatusList(projectId, PmsWorkItemTypeEnum.TASK.getType()); + + // 断言:读取接口只查询已保存数据,不会重新创建被删除的系统状态 + assertEquals(2, recreatedStatuses.size()); + assertFalse(recreatedStatuses.stream().anyMatch(status -> + PmsWorkItemStatusTypeEnum.PROCESSING.name().equals(status.getSystemCode()))); + } + + @Test + public void testValidateWorkItemStatus_projectMismatch() { + // mock 数据 + PmsWorkItemStatusDO status = new PmsWorkItemStatusDO().setProjectId(randomLongId()) + .setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()).setName("未开始") + .setStatusType(PmsWorkItemStatusTypeEnum.PENDING.getType()).setDefaultStatus(true).setSort(1); + workItemStatusMapper.insert(status); + + // 调用,并断言异常 + assertServiceException(() -> workItemStatusService.validateWorkItemStatus(status.getId(), randomLongId(), + PmsWorkItemTypeEnum.TASK.getType()), WORK_ITEM_STATUS_INVALID); + } + + @Test + public void testCreateAndUpdateWorkItemStatus_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.AGILE.getType()); + // 准备参数并创建 + Long statusId = workItemStatusService.createWorkItemStatus(new PmsWorkItemStatusCreateReqVO() + .setProjectId(projectId).setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()) + .setName("待验收").setStatusType(PmsWorkItemStatusTypeEnum.PROCESSING.getType()) + .setDescription("等待产品验收"), userId); + PmsWorkItemDO workItem = new PmsWorkItemDO().setProjectId(projectId) + .setType(PmsWorkItemTypeEnum.TASK.getType()).setSerialNumber(1).setName("任务") + .setPriority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()).setStatusId(statusId) + .setStatus(PmsWorkItemStatusTypeEnum.PROCESSING.getType()).setProgress(0).setSort(1); + workItemMapper.insert(workItem); + + // 调用更新 + workItemStatusService.updateWorkItemStatusConfig(new PmsWorkItemStatusConfigUpdateReqVO().setId(statusId) + .setName("已验收").setStatusType(PmsWorkItemStatusTypeEnum.COMPLETED.getType()) + .setDescription("产品验收完成"), userId); + + // 断言 + assertEquals("已验收", workItemStatusMapper.selectById(statusId).getName()); + assertEquals("产品验收完成", workItemStatusMapper.selectById(statusId).getDescription()); + assertNull(workItemStatusMapper.selectById(statusId).getBoardName()); + assertEquals(PmsWorkItemStatusTypeEnum.COMPLETED.getType(), + workItemMapper.selectById(workItem.getId()).getStatus()); + } + + @Test + public void testUpdateWorkItemBoardConfig_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + List statuses = workItemStatusService.getWorkItemStatusList(projectId, + PmsWorkItemTypeEnum.TASK.getType()); + // 准备参数 + PmsWorkItemBoardConfigSaveReqVO reqVO = new PmsWorkItemBoardConfigSaveReqVO() + .setProjectId(projectId).setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()) + .setBoards(Arrays.asList( + new PmsWorkItemBoardConfigSaveReqVO.Board().setName("待处理") + .setStatusIds(Collections.singletonList(statuses.get(0).getId())), + new PmsWorkItemBoardConfigSaveReqVO.Board().setName("处理中") + .setStatusIds(Arrays.asList(statuses.get(1).getId(), statuses.get(2).getId())), + new PmsWorkItemBoardConfigSaveReqVO.Board().setName("待发布") + .setStatusIds(Collections.emptyList()))); + + // 调用 + workItemStatusService.updateWorkItemBoardConfig(reqVO, userId); + + // 断言 + List boards = workItemBoardMapper + .selectListByProjectIdAndWorkItemType(projectId, PmsWorkItemTypeEnum.TASK.getType()); + assertEquals(3, boards.size()); + assertEquals("待发布", boards.get(2).getName()); + assertEquals("待处理", workItemStatusMapper.selectById(statuses.get(0).getId()).getBoardName()); + assertEquals("处理中", workItemStatusMapper.selectById(statuses.get(2).getId()).getBoardName()); + } + + @Test + public void testCreateWorkItemStatus_nameDuplicate() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + // 调用,并断言异常:已初始化的系统状态不能重复创建 + assertServiceException(() -> workItemStatusService.createWorkItemStatus(new PmsWorkItemStatusCreateReqVO() + .setProjectId(projectId).setWorkItemType(PmsWorkItemTypeEnum.TASK.getType()) + .setName("未开始").setStatusType(PmsWorkItemStatusTypeEnum.PENDING.getType()), userId), + WORK_ITEM_STATUS_NAME_DUPLICATE); + } + + @Test + public void testUpdateDefaultAndSortWorkItemStatuses_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + List statuses = workItemStatusService.getWorkItemStatusList(projectId, + PmsWorkItemTypeEnum.TASK.getType()); + + // 调用 + workItemStatusService.updateDefaultWorkItemStatus(statuses.get(1).getId(), userId); + workItemStatusService.updateWorkItemStatusSort(new PmsWorkItemStatusSortReqVO().setStatusIds(Arrays.asList( + statuses.get(2).getId(), statuses.get(1).getId(), CollUtil.getFirst(statuses).getId())), userId); + + // 断言 + List results = workItemStatusMapper + .selectListByProjectIdAndWorkItemType(projectId, PmsWorkItemTypeEnum.TASK.getType()); + assertEquals(statuses.get(2).getId(), CollUtil.getFirst(results).getId()); + assertTrue(workItemStatusMapper.selectById(statuses.get(1).getId()).getDefaultStatus()); + } + + @Test + public void testDeleteWorkItemStatus_transferWorkItems() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + List statuses = workItemStatusService.getWorkItemStatusList(projectId, + PmsWorkItemTypeEnum.TASK.getType()); + PmsWorkItemDO workItem = new PmsWorkItemDO().setProjectId(projectId) + .setType(PmsWorkItemTypeEnum.TASK.getType()).setSerialNumber(1).setName("任务") + .setPriority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()).setStatusId(statuses.get(1).getId()) + .setStatus(statuses.get(1).getStatusType()).setProgress(0).setSort(1); + workItemMapper.insert(workItem); + + // 调用 + workItemStatusService.deleteWorkItemStatus(new PmsWorkItemStatusDeleteReqVO().setId(statuses.get(1).getId()) + .setTransferStatusId(statuses.get(2).getId()), userId); + + // 断言 + assertNull(workItemStatusMapper.selectById(statuses.get(1).getId())); + assertEquals(statuses.get(2).getId(), workItemMapper.selectById(workItem.getId()).getStatusId()); + assertEquals(statuses.get(2).getStatusType(), workItemMapper.selectById(workItem.getId()).getStatus()); + } + + @Test + public void testDeleteWorkItemStatus_defaultStatus() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + PmsWorkItemStatusDO defaultStatus = CollUtil.getFirst(workItemStatusService.getWorkItemStatusList(projectId, + PmsWorkItemTypeEnum.TASK.getType())); + + // 调用,并断言异常 + assertServiceException(() -> workItemStatusService.deleteWorkItemStatus(new PmsWorkItemStatusDeleteReqVO() + .setId(defaultStatus.getId()), userId), WORK_ITEM_STATUS_DEFAULT_CANNOT_DELETE); + } + + @Test + public void testDeleteWorkItemStatus_missingTransferStatus() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + when(projectMemberService.validateProjectWritable(projectId, userId)) + .thenReturn(randomProjectDO(projectId)); + workItemStatusService.initProjectWorkItemStatuses(projectId, PmsProjectTypeEnum.GENERAL.getType()); + List statuses = workItemStatusService.getWorkItemStatusList(projectId, + PmsWorkItemTypeEnum.TASK.getType()); + PmsWorkItemDO workItem = new PmsWorkItemDO().setProjectId(projectId) + .setType(PmsWorkItemTypeEnum.TASK.getType()).setSerialNumber(1).setName("任务") + .setPriority(PmsWorkItemPriorityEnum.MEDIUM.getPriority()).setStatusId(statuses.get(1).getId()) + .setStatus(statuses.get(1).getStatusType()).setProgress(0).setSort(1); + workItemMapper.insert(workItem); + + // 调用,并断言异常 + assertServiceException(() -> workItemStatusService.deleteWorkItemStatus(new PmsWorkItemStatusDeleteReqVO() + .setId(statuses.get(1).getId()), userId), WORK_ITEM_STATUS_TRANSFER_INVALID); + } + + // ========== 随机对象 ========== + + private static PmsProjectDO randomProjectDO(Long id) { + return new PmsProjectDO().setId(id).setName("测试项目") + .setStatus(PmsProjectStatusEnum.ACTIVE.getStatus()).setType(PmsProjectTypeEnum.AGILE.getType()) + .setLevel(3).setOpenStatus(false); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogServiceImplTest.java b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogServiceImplTest.java new file mode 100644 index 000000000..3f78aff1a --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemWorkLogServiceImplTest.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.module.pms.service.pm.workitem; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsProjectWorkLogReportReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsProjectWorkLogReportRespVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSaveReqVO; +import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.worklog.PmsWorkItemWorkLogSummaryRespVO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.iteration.PmsIterationDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemWorkLogMapper; +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.WORK_ITEM_WORK_LOG_INVALID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link PmsWorkItemWorkLogServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(PmsWorkItemWorkLogServiceImpl.class) +public class PmsWorkItemWorkLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private PmsWorkItemWorkLogServiceImpl workLogService; + + @Resource + private PmsWorkItemWorkLogMapper workLogMapper; + + @MockitoBean + private PmsWorkItemService workItemService; + @MockitoBean + private PmsProjectMemberService projectMemberService; + @MockitoBean + private PmsWorkItemActivityService workItemActivityService; + @MockitoBean + private PmsIterationService iterationService; + + @Test + public void testCreateWorkItemWorkLog_success() { + // mock 数据 + Long userId = randomLongId(); + PmsWorkItemDO workItem = randomWorkItemDO(); + when(workItemService.getWritableWorkItem(workItem.getId(), userId)).thenReturn(workItem); + when(projectMemberService.validateProjectWritable(workItem.getProjectId(), userId)) + .thenReturn(new PmsProjectDO().setId(workItem.getProjectId())); + // 准备参数 + PmsWorkItemWorkLogSaveReqVO reqVO = new PmsWorkItemWorkLogSaveReqVO() + .setWorkItemId(workItem.getId()).setActualHours(4).setRemainingHours(6) + .setDescription(" 完成接口联调 "); + + // 调用 + Long id = workLogService.createWorkItemWorkLog(reqVO, userId); + + // 断言 + PmsWorkItemWorkLogDO workLog = workLogMapper.selectById(id); + assertEquals(workItem.getProjectId(), workLog.getProjectId()); + assertEquals(4, workLog.getActualHours()); + assertEquals(6, workLog.getRemainingHours()); + assertEquals("完成接口联调", workLog.getDescription()); + } + + @Test + public void testUpdateWorkItemWorkLog_success() { + // mock 数据 + Long userId = randomLongId(); + PmsWorkItemDO workItem = randomWorkItemDO(); + PmsWorkItemWorkLogDO workLog = new PmsWorkItemWorkLogDO().setProjectId(workItem.getProjectId()) + .setWorkItemId(workItem.getId()).setActualHours(2).setRemainingHours(8).setDescription("旧说明"); + workLogMapper.insert(workLog); + when(workItemService.getWritableWorkItem(workItem.getId(), userId)).thenReturn(workItem); + when(projectMemberService.validateProjectWritable(workItem.getProjectId(), userId)) + .thenReturn(new PmsProjectDO().setId(workItem.getProjectId())); + // 准备参数 + PmsWorkItemWorkLogSaveReqVO reqVO = new PmsWorkItemWorkLogSaveReqVO().setId(workLog.getId()) + .setWorkItemId(workItem.getId()).setActualHours(5).setRemainingHours(3).setDescription(null); + + // 调用 + workLogService.updateWorkItemWorkLog(reqVO, userId); + + // 断言 + PmsWorkItemWorkLogDO updatedWorkLog = workLogMapper.selectById(workLog.getId()); + assertEquals(5, updatedWorkLog.getActualHours()); + assertEquals(3, updatedWorkLog.getRemainingHours()); + assertNull(updatedWorkLog.getDescription()); + } + + @Test + public void testUpdateWorkItemWorkLog_workItemMismatch() { + // mock 数据 + PmsWorkItemDO workItem = randomWorkItemDO(); + PmsWorkItemWorkLogDO workLog = new PmsWorkItemWorkLogDO().setProjectId(workItem.getProjectId()) + .setWorkItemId(workItem.getId()).setActualHours(2).setRemainingHours(8); + workLogMapper.insert(workLog); + // 准备参数 + PmsWorkItemWorkLogSaveReqVO reqVO = new PmsWorkItemWorkLogSaveReqVO().setId(workLog.getId()) + .setWorkItemId(randomLongId()).setActualHours(5).setRemainingHours(3); + + // 调用,并断言异常 + assertServiceException(() -> workLogService.updateWorkItemWorkLog(reqVO, randomLongId()), + WORK_ITEM_WORK_LOG_INVALID); + } + + @Test + public void testGetWorkItemWorkLogSummary_success() { + // mock 数据 + Long userId = randomLongId(); + PmsWorkItemDO workItem = randomWorkItemDO().setEstimatedHours(10); + when(workItemService.getWorkItem(workItem.getId(), userId)).thenReturn(workItem); + workLogMapper.insert(new PmsWorkItemWorkLogDO().setProjectId(workItem.getProjectId()) + .setWorkItemId(workItem.getId()).setActualHours(3).setRemainingHours(7)); + workLogMapper.insert(new PmsWorkItemWorkLogDO().setProjectId(workItem.getProjectId()) + .setWorkItemId(workItem.getId()).setActualHours(4).setRemainingHours(2)); + + // 调用 + PmsWorkItemWorkLogSummaryRespVO summary = workLogService.getWorkItemWorkLogSummary(workItem.getId(), userId); + + // 断言 + assertEquals(10, summary.getEstimatedHours()); + assertEquals(7, summary.getActualHours()); + assertEquals(2, summary.getRemainingHours()); + assertEquals(2, summary.getRecords().size()); + } + + @Test + public void testGetProjectWorkItemWorkLogReport_success() { + // mock 数据 + Long userId = randomLongId(); + Long projectId = randomLongId(); + Long iterationId = randomLongId(); + PmsWorkItemDO workItem = randomWorkItemDO().setProjectId(projectId).setIterationId(iterationId) + .setSerialNumber(1); + workLogMapper.insert(new PmsWorkItemWorkLogDO().setProjectId(projectId) + .setWorkItemId(workItem.getId()).setActualHours(3)); + workLogMapper.insert(new PmsWorkItemWorkLogDO().setProjectId(projectId) + .setWorkItemId(workItem.getId()).setActualHours(4)); + when(workItemService.getWorkItemList(Collections.singleton(workItem.getId()))) + .thenReturn(Collections.singletonList(workItem)); + when(iterationService.getIterationList(Collections.singleton(iterationId))) + .thenReturn(Collections.singletonList(new PmsIterationDO().setId(iterationId).setName("第一迭代"))); + + // 调用 + LocalDate today = LocalDate.now(); + PmsProjectWorkLogReportReqVO reqVO = new PmsProjectWorkLogReportReqVO().setProjectId(projectId) + .setCreateTime(new LocalDateTime[]{today.minusDays(1).atStartOfDay(), today.atTime(LocalTime.MAX)}) + .setIterationName("第一"); + PmsProjectWorkLogReportRespVO report = workLogService.getProjectWorkItemWorkLogReport( + reqVO, userId); + + // 断言 + assertEquals(2, report.getDates().size()); + assertEquals(today.minusDays(1).toString(), CollUtil.getFirst(report.getDates())); + assertEquals(today.toString(), report.getDates().get(1)); + assertEquals(7, report.getTotalHours()); + assertEquals(1, report.getGroups().size()); + assertEquals("第一迭代", CollUtil.getFirst(report.getGroups()).getIterationName()); + assertEquals(7, CollUtil.getFirst(CollUtil.getFirst(report.getGroups()).getItems()).getTotalHours()); + verify(projectMemberService).validateProjectReadable(projectId, userId); + } + + // ========== 随机对象 ========== + + private static PmsWorkItemDO randomWorkItemDO() { + return new PmsWorkItemDO().setId(randomLongId()).setProjectId(randomLongId()).setName("测试任务"); + } + +} diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/resources/application-unit-test.yaml b/yudao-module-pms/yudao-module-pms-server/src/test/resources/application-unit-test.yaml new file mode 100644 index 000000000..f0fcc544d --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/resources/application-unit-test.yaml @@ -0,0 +1,36 @@ +spring: + main: + lazy-initialization: true + banner-mode: off + allow-bean-definition-overriding: true + +--- #################### 数据库相关配置 #################### + +spring: + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value,day,year,month; + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true + initial-size: 1 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + data: + redis: + host: 127.0.0.1 + port: 16379 + database: 0 + +mybatis: + lazy-initialization: true + +--- #################### 芋道相关配置 #################### + +yudao: + info: + base-package: cn.iocoder.yudao.module diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/resources/sql/clean.sql b/yudao-module-pms/yudao-module-pms-server/src/test/resources/sql/clean.sql new file mode 100644 index 000000000..c8a8df116 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/resources/sql/clean.sql @@ -0,0 +1,14 @@ +DELETE FROM "pms_knowledge_library_member"; +DELETE FROM "pms_knowledge_library"; +DELETE FROM "pms_work_item_activity"; +DELETE FROM "pms_work_item_comment"; +DELETE FROM "pms_work_item_label"; +DELETE FROM "pms_work_item_work_log"; +DELETE FROM "pms_work_item_member"; +DELETE FROM "pms_work_item"; +DELETE FROM "pms_work_item_status"; +DELETE FROM "pms_iteration"; +DELETE FROM "pms_project_group_relation"; +DELETE FROM "pms_project_group"; +DELETE FROM "pms_project_member"; +DELETE FROM "pms_project"; diff --git a/yudao-module-pms/yudao-module-pms-server/src/test/resources/sql/create_tables.sql b/yudao-module-pms/yudao-module-pms-server/src/test/resources/sql/create_tables.sql new file mode 100644 index 000000000..ba6f9f111 --- /dev/null +++ b/yudao-module-pms/yudao-module-pms-server/src/test/resources/sql/create_tables.sql @@ -0,0 +1,592 @@ +CREATE TABLE IF NOT EXISTS "pms_project" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(50) NOT NULL, + "status" int NOT NULL DEFAULT 1, + "type" int NOT NULL, + "level" int NOT NULL, + "description" text DEFAULT NULL, + "open_status" bit NOT NULL DEFAULT TRUE, + "icon" varchar(255) DEFAULT NULL, + "sort" int DEFAULT NULL, + "start_time" timestamp DEFAULT NULL, + "end_time" timestamp DEFAULT NULL, + "archive_time" timestamp DEFAULT NULL, + "recycle_time" timestamp DEFAULT NULL, + "access_time" timestamp DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_project_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL, + "description" varchar(500) DEFAULT NULL, + "project_type" int NOT NULL, + "status" tinyint NOT NULL DEFAULT 0, + "sort" int NOT NULL DEFAULT 0, + "item_types" varchar(5000) NOT NULL, + "statuses" varchar(10000) NOT NULL, + "boards" varchar(10000) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_library_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL, + "description" varchar(500) DEFAULT NULL, + "cover_url" varchar(255) DEFAULT NULL, + "status" tinyint NOT NULL DEFAULT 0, + "sort" int NOT NULL DEFAULT 0, + "documents" varchar(1000000) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_project_member" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "level" tinyint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_project_group" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "name" varchar(100) NOT NULL, + "sort" int NOT NULL DEFAULT 0, + "type" tinyint NOT NULL DEFAULT 3, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_project_group_relation" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "group_id" bigint NOT NULL, + "project_id" bigint NOT NULL, + "sort" int NOT NULL DEFAULT 0, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_project_favorite" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_project_announcement" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "content" text NOT NULL, + "file_urls" varchar(5000) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_iteration" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "name" varchar(100) NOT NULL, + "owner_user_id" bigint DEFAULT NULL, + "status" int NOT NULL DEFAULT 1, + "start_time" timestamp DEFAULT NULL, + "end_time" timestamp DEFAULT NULL, + "finish_time" timestamp DEFAULT NULL, + "target" varchar(255) DEFAULT NULL, + "description" text DEFAULT NULL, + "sort" int NOT NULL DEFAULT 0, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item_status" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "work_item_type" int NOT NULL, + "name" varchar(50) NOT NULL, + "status_type" int NOT NULL, + "description" varchar(255) DEFAULT NULL, + "board_name" varchar(50) DEFAULT NULL, + "system_code" varchar(20) DEFAULT NULL, + "default_status" bit NOT NULL DEFAULT FALSE, + "sort" int NOT NULL DEFAULT 0, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item_board" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "work_item_type" int NOT NULL, + "name" varchar(50) NOT NULL, + "sort" int NOT NULL DEFAULT 0, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "type" int NOT NULL, + "serial_number" int NOT NULL, + "name" varchar(100) NOT NULL, + "description" text DEFAULT NULL, + "priority" int NOT NULL, + "assignee_user_id" bigint DEFAULT NULL, + "status_id" bigint NOT NULL, + "status" int NOT NULL, + "lifecycle_status" int NOT NULL DEFAULT 1, + "archive_time" timestamp DEFAULT NULL, + "recycle_time" timestamp DEFAULT NULL, + "iteration_id" bigint DEFAULT NULL, + "parent_id" bigint DEFAULT NULL, + "related_requirement_id" bigint DEFAULT NULL, + "defect_type" int DEFAULT NULL, + "start_time" timestamp DEFAULT NULL, + "end_time" timestamp DEFAULT NULL, + "estimated_hours" int DEFAULT NULL, + "progress" int NOT NULL DEFAULT 0, + "file_urls" varchar(5000) DEFAULT NULL, + "label_ids" varchar(5000) DEFAULT NULL, + "sort" int NOT NULL DEFAULT 0, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item_member" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "work_item_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item_user_sort" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "work_item_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "sort" int NOT NULL DEFAULT 0, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item_work_log" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "work_item_id" bigint NOT NULL, + "actual_hours" int NOT NULL, + "remaining_hours" int NOT NULL DEFAULT 0, + "description" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item_label" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(50) NOT NULL, + "color" varchar(20) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item_comment" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "work_item_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "main_id" bigint NOT NULL DEFAULT 0, + "reply_user_id" bigint DEFAULT NULL, + "content" varchar(2000) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_work_item_activity" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "project_id" bigint NOT NULL, + "work_item_id" bigint NOT NULL, + "operator_user_id" bigint NOT NULL, + "content" varchar(500) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_library" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL, + "description" varchar(300) DEFAULT NULL, + "open_status" bit NOT NULL DEFAULT FALSE, + "cover_url" varchar(255) DEFAULT NULL, + "status" int NOT NULL DEFAULT 1, + "delete_user_id" bigint DEFAULT NULL, + "delete_time" timestamp DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_library_member" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "library_id" bigint NOT NULL, + "user_id" bigint DEFAULT NULL, + "dept_id" bigint DEFAULT NULL, + "level" int NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_folder" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "library_id" bigint NOT NULL, + "permission_id" bigint NOT NULL, + "parent_id" bigint NOT NULL DEFAULT 0, + "title" varchar(255) NOT NULL, + "status" int NOT NULL DEFAULT 1, + "delete_user_id" bigint DEFAULT NULL, + "delete_time" timestamp DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_document" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "library_id" bigint NOT NULL, + "permission_id" bigint NOT NULL, + "folder_id" bigint NOT NULL DEFAULT 0, + "parent_id" bigint NOT NULL DEFAULT 0, + "title" varchar(255) NOT NULL, + "content" text DEFAULT NULL, + "type" int NOT NULL, + "file_type" varchar(50) DEFAULT NULL, + "file_size" bigint DEFAULT NULL, + "status" int NOT NULL DEFAULT 1, + "label_ids" varchar(5000) DEFAULT NULL, + "delete_user_id" bigint DEFAULT NULL, + "delete_time" timestamp DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_content_permission" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "library_id" bigint NOT NULL, + "open_status" bit NOT NULL DEFAULT TRUE, + "open_level" tinyint NOT NULL DEFAULT 3, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_content_permission_member" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "permission_id" bigint NOT NULL, + "user_id" bigint DEFAULT NULL, + "dept_id" bigint DEFAULT NULL, + "level" tinyint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_document_comment" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "document_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "main_id" bigint NOT NULL DEFAULT 0, + "reply_user_id" bigint DEFAULT NULL, + "content" varchar(2000) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_recycle_record" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "library_id" bigint NOT NULL, + "type" int NOT NULL, + "entity_id" bigint NOT NULL, + "name" varchar(255) NOT NULL, + "delete_user_id" bigint NOT NULL, + "delete_time" timestamp NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); +CREATE INDEX IF NOT EXISTS "idx_tenant_type_entity" ON "pms_knowledge_recycle_record" + ("tenant_id", "type", "entity_id"); +CREATE INDEX IF NOT EXISTS "idx_tenant_delete_time_id" ON "pms_knowledge_recycle_record" + ("tenant_id", "delete_time", "id"); +CREATE INDEX IF NOT EXISTS "idx_tenant_library_delete_time" ON "pms_knowledge_recycle_record" + ("tenant_id", "library_id", "delete_time"); +CREATE INDEX IF NOT EXISTS "idx_tenant_delete_user_type" ON "pms_knowledge_recycle_record" + ("tenant_id", "delete_user_id", "type"); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_favorite" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "library_id" bigint NOT NULL, + "type" int NOT NULL, + "entity_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); +CREATE INDEX IF NOT EXISTS "idx_favorite_tenant_user_create_time_id" ON "pms_knowledge_favorite" + ("tenant_id", "user_id", "create_time", "id"); +CREATE INDEX IF NOT EXISTS "idx_favorite_tenant_user_library_create_time_id" ON "pms_knowledge_favorite" + ("tenant_id", "user_id", "library_id", "create_time", "id"); +CREATE INDEX IF NOT EXISTS "idx_favorite_tenant_user_type_entity" ON "pms_knowledge_favorite" + ("tenant_id", "user_id", "type", "entity_id"); +CREATE INDEX IF NOT EXISTS "idx_favorite_tenant_library" ON "pms_knowledge_favorite" + ("tenant_id", "library_id"); +CREATE INDEX IF NOT EXISTS "idx_favorite_tenant_type_entity" ON "pms_knowledge_favorite" + ("tenant_id", "type", "entity_id"); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_document_like" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "document_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); +CREATE INDEX IF NOT EXISTS "idx_like_tenant_document_create_time" ON "pms_knowledge_document_like" + ("tenant_id", "document_id", "create_time"); +CREATE INDEX IF NOT EXISTS "idx_like_tenant_user_document" ON "pms_knowledge_document_like" + ("tenant_id", "user_id", "document_id"); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_view_record" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "library_id" bigint NOT NULL, + "type" int NOT NULL, + "entity_id" bigint NOT NULL, + "user_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); +CREATE INDEX IF NOT EXISTS "idx_view_tenant_user_create_time_id" ON "pms_knowledge_view_record" + ("tenant_id", "user_id", "create_time", "id"); +CREATE INDEX IF NOT EXISTS "idx_view_tenant_user_type_entity" ON "pms_knowledge_view_record" + ("tenant_id", "user_id", "type", "entity_id"); +CREATE INDEX IF NOT EXISTS "idx_view_tenant_library_create_time_id" ON "pms_knowledge_view_record" + ("tenant_id", "library_id", "create_time", "id"); +CREATE INDEX IF NOT EXISTS "idx_view_tenant_type_entity" ON "pms_knowledge_view_record" + ("tenant_id", "type", "entity_id"); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_document_share" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "document_id" bigint NOT NULL, + "share_user_ids" varchar(5000) DEFAULT NULL, + "token" varchar(64) NOT NULL, + "status" tinyint NOT NULL DEFAULT 0, + "close_user_id" bigint DEFAULT NULL, + "close_time" timestamp DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id"), + UNIQUE ("token") +); +CREATE INDEX IF NOT EXISTS "idx_share_tenant_document" ON "pms_knowledge_document_share" + ("tenant_id", "document_id"); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_document_label" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL, + "color" varchar(20) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_group" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "name" varchar(100) NOT NULL, + "sort" int NOT NULL DEFAULT 0, + "type" tinyint NOT NULL DEFAULT 3, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); +CREATE INDEX IF NOT EXISTS "idx_group_tenant_user_type" ON "pms_knowledge_group" + ("tenant_id", "user_id", "type"); +CREATE INDEX IF NOT EXISTS "idx_group_tenant_user_sort" ON "pms_knowledge_group" + ("tenant_id", "user_id", "sort"); + +CREATE TABLE IF NOT EXISTS "pms_knowledge_group_relation" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "group_id" bigint NOT NULL, + "library_id" bigint NOT NULL, + "sort" int NOT NULL DEFAULT 0, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); +CREATE INDEX IF NOT EXISTS "idx_group_relation_tenant_user_group_library" ON "pms_knowledge_group_relation" + ("tenant_id", "user_id", "group_id", "library_id"); +CREATE INDEX IF NOT EXISTS "idx_group_relation_tenant_user_library" ON "pms_knowledge_group_relation" + ("tenant_id", "user_id", "library_id"); +CREATE INDEX IF NOT EXISTS "idx_group_relation_tenant_library" ON "pms_knowledge_group_relation" + ("tenant_id", "library_id"); diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index d952fbd2e..1c548ea90 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -148,6 +148,13 @@ + + + + + + + @@ -188,6 +195,13 @@ + + + org.springframework.boot + spring-boot-starter-test + test + + diff --git a/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java b/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java index 59dd1def4..ee5bb9df8 100644 --- a/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java +++ b/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java @@ -53,6 +53,24 @@ public class DefaultController { "[WMS 仓库管理系统 yudao-module-wms - 已禁用][参考 https://doc.iocoder.cn/wms/build/ 开启]"); } + @RequestMapping("/admin-api/hrm/**") + public CommonResult hrm404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[HRM 人力资源管理系统 yudao-module-hrm - 已禁用][参考 https://cloud.iocoder.cn/hrm/build/ 开启]"); + } + + @RequestMapping("/admin-api/fms/**") + public CommonResult fms404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[FMS 财务管理系统 yudao-module-fms - 已禁用][参考 https://cloud.iocoder.cn/fms/build/ 开启]"); + } + + @RequestMapping("/admin-api/pms/**") + public CommonResult pms404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[PMS 项目管理系统 yudao-module-pms - 已禁用][参考 https://cloud.iocoder.cn/pms/build/ 开启]"); + } + @RequestMapping("/admin-api/crm/**") public CommonResult crm404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(),