From 117dd91afdd21bb5e6685d7decfb2859321148a6 Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Wed, 17 Jun 2026 21:01:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor(config):=20=E5=B0=86=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=BF=81=E7=A7=BB=E8=87=B3=20Nacos=20?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E9=80=9A=E7=9F=A5=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 application-dev.yaml 和 application-local.yaml 配置文件 - 将 Nacos 配置外置到根 pom 的 nacos.* 属性中 - 添加配置中心文件加载配置(rdms-common.yaml、rdms-common-secret.yaml) - 网关服务仅用 Nacos 做服务发现,不加载配置中心文件 - 为系统服务添加独有敏感配置(rdms-system-server-secret.yaml) - 为 mapper 添加 SQL 日志打印配置 - 为 NotifySendEvent 添加操作人用户编号字段用于排除通知 - 修改 NotifySendEvent 构造函数支持操作人排除参数 - 在通知监听器中实现操作人排除逻辑 - 添加操作人排除功能的单元测试 --- pom.xml | 48 +++++++ rdms-gateway/pom.xml | 6 - .../src/main/resources/application-dev.yaml | 23 ---- .../src/main/resources/application-local.yaml | 29 ---- .../src/main/resources/application.yaml | 26 ++-- rdms-project/rdms-project-boot/pom.xml | 4 +- .../admin/common/vo/CurrentUserRoleVO.java | 20 +++ .../admin/product/ProductController.java | 4 +- .../product/vo/product/ProductRespVO.java | 5 + .../project/vo/project/ProjectRespVO.java | 4 + .../mysql/member/UserObjectRoleMapper.java | 16 +++ .../framework/notify/NotifySendEvent.java | 23 +++- .../notify/NotifySendEventListener.java | 4 + .../notify/NotifyTemplateCodeConstants.java | 15 +++ .../notify/TeamNotifyRecipientResolver.java | 71 ++++++++++ .../framework/notify/TeamRecipient.java | 4 + .../member/CurrentUserRoleResolver.java | 96 +++++++++++++ .../ObjectRoleAutoAssignServiceImpl.java | 16 ++- .../ProductRequirementServiceImpl.java | 31 +++++ .../service/product/ProductService.java | 5 +- .../service/product/ProductServiceImpl.java | 64 ++++++++- .../ProjectRequirementServiceImpl.java | 30 +++++ .../service/project/ProjectServiceImpl.java | 46 +++++++ .../ProjectExecutionServiceImpl.java | 28 ++++ .../project/task/ProjectTaskService.java | 14 ++ .../project/task/ProjectTaskServiceImpl.java | 23 +++- .../task/worklog/TaskWorklogServiceImpl.java | 6 +- .../src/main/resources/application-dev.yaml | 92 ------------- .../src/main/resources/application-local.yaml | 98 -------------- .../src/main/resources/application.yaml | 25 +++- .../main/resources/sql/due_alert_record.sql | 19 --- .../sql/td015_project_completion_index.sql | 24 ---- .../notify/NotifySendEventListenerTest.java | 13 ++ .../TeamNotifyRecipientResolverTest.java | 126 ++++++++++++++++++ .../ProductRequirementServiceImplTest.java | 51 +++++++ .../product/ProductServiceImplTest.java | 37 ++++- .../ProjectRequirementServiceImplTest.java | 58 ++++++++ .../project/ProjectServiceImplTest.java | 35 +++++ .../ProjectExecutionServiceImplTest.java | 36 +++++ .../task/ProjectTaskServiceImplTest.java | 25 ++++ rdms-system/rdms-system-boot/pom.xml | 4 +- .../src/main/resources/application-dev.yaml | 92 ------------- .../src/main/resources/application-local.yaml | 100 -------------- .../src/main/resources/application.yaml | 30 ++++- 44 files changed, 998 insertions(+), 528 deletions(-) delete mode 100644 rdms-gateway/src/main/resources/application-dev.yaml delete mode 100644 rdms-gateway/src/main/resources/application-local.yaml create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/common/vo/CurrentUserRoleVO.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/TeamNotifyRecipientResolver.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/TeamRecipient.java create mode 100644 rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/member/CurrentUserRoleResolver.java delete mode 100644 rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml delete mode 100644 rdms-project/rdms-project-boot/src/main/resources/application-local.yaml delete mode 100644 rdms-project/rdms-project-boot/src/main/resources/sql/due_alert_record.sql delete mode 100644 rdms-project/rdms-project-boot/src/main/resources/sql/td015_project_completion_index.sql create mode 100644 rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/notify/TeamNotifyRecipientResolverTest.java delete mode 100644 rdms-system/rdms-system-boot/src/main/resources/application-dev.yaml delete mode 100644 rdms-system/rdms-system-boot/src/main/resources/application-local.yaml diff --git a/pom.xml b/pom.xml index f17a777..454da5d 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,16 @@ 1.6.3 UTF-8 2.2.38 + 3.3.1 + + + 192.168.1.103:18848 + 1924bcfb-4eab-4c58-9003-4a37d5fc2949 + DEFAULT_GROUP + + @@ -133,8 +143,46 @@ + + + + src/main/resources + true + + application*.yml + application*.yaml + bootstrap*.yml + bootstrap*.yaml + + + + src/main/resources + false + + application*.yml + application*.yaml + bootstrap*.yml + bootstrap*.yaml + + + + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + false + + @ + + + diff --git a/rdms-gateway/pom.xml b/rdms-gateway/pom.xml index c0bdc96..2bfa8c5 100644 --- a/rdms-gateway/pom.xml +++ b/rdms-gateway/pom.xml @@ -54,12 +54,6 @@ spring-cloud-starter-alibaba-nacos-discovery - - - com.alibaba.cloud - spring-cloud-starter-alibaba-nacos-config - - com.google.guava diff --git a/rdms-gateway/src/main/resources/application-dev.yaml b/rdms-gateway/src/main/resources/application-dev.yaml deleted file mode 100644 index 7a2d55a..0000000 --- a/rdms-gateway/src/main/resources/application-dev.yaml +++ /dev/null @@ -1,23 +0,0 @@ -#################### 注册中心 + 配置中心相关配置 #################### -spring: - cloud: - nacos: - server-addr: 192.168.1.103:18848 # Nacos 服务器地址 - username: # Nacos 账号 - password: # Nacos 密码 - discovery: # 【配置中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - config: # 【注册中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - -#################### 监控相关配置 #################### -# Actuator 监控端点的配置项 -management: - endpoints: - web: - base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator - exposure: - include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 - diff --git a/rdms-gateway/src/main/resources/application-local.yaml b/rdms-gateway/src/main/resources/application-local.yaml deleted file mode 100644 index 811726f..0000000 --- a/rdms-gateway/src/main/resources/application-local.yaml +++ /dev/null @@ -1,29 +0,0 @@ -#################### 注册中心 + 配置中心相关配置 #################### -spring: - cloud: - nacos: - server-addr: 192.168.1.103:18848 # Nacos 服务器地址 - username: # Nacos 账号 - password: # Nacos 密码 - discovery: # 【配置中心】配置项 - namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - config: # 【注册中心】配置项 - namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - -#################### 监控相关配置 #################### -# Actuator 监控端点的配置项 -management: - endpoints: - web: - base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator - exposure: - include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 - - -# 日志文件配置 -logging: - level: - org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR - diff --git a/rdms-gateway/src/main/resources/application.yaml b/rdms-gateway/src/main/resources/application.yaml index 99ff434..2b34ace 100644 --- a/rdms-gateway/src/main/resources/application.yaml +++ b/rdms-gateway/src/main/resources/application.yaml @@ -2,9 +2,6 @@ spring: application: name: gateway-server - profiles: - active: local - http: codecs: max-in-memory-size: 10MB # 调整缓冲区大小https://gitee.com/zhijiantianya/rdms-cloud/pulls/176 @@ -20,12 +17,15 @@ spring: main: allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 - config: - import: - - optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置 - - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 - cloud: + # 注册中心连接(值由根 pom 的 nacos.* 属性在打包时注入)。网关仅用 Nacos 做服务发现,不加载配置中心文件。 + nacos: + server-addr: @nacos.server-addr@ + username: @nacos.username@ + password: @nacos.password@ + discovery: + namespace: @nacos.namespace@ + group: @nacos.group@ # Spring Cloud Gateway 配置项,对应 GatewayProperties 类 gateway: server: @@ -89,6 +89,16 @@ server: logging: file: name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录 + exposure: + include: '*' # 开放所有端点 knife4j: # 聚合 Swagger 文档,参考 https://doc.xiaominfo.com/docs/action/springcloud-gateway 文档 diff --git a/rdms-project/rdms-project-boot/pom.xml b/rdms-project/rdms-project-boot/pom.xml index e6354e2..22dee7f 100644 --- a/rdms-project/rdms-project-boot/pom.xml +++ b/rdms-project/rdms-project-boot/pom.xml @@ -113,7 +113,9 @@ spring-boot-maven-plugin ${spring.boot.version} - true + + false diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/common/vo/CurrentUserRoleVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/common/vo/CurrentUserRoleVO.java new file mode 100644 index 0000000..1e94e86 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/common/vo/CurrentUserRoleVO.java @@ -0,0 +1,20 @@ +package com.njcn.rdms.module.project.controller.admin.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 当前登录用户在某对象(产品 / 项目)上担任的单个角色。 + * 供产品列表、项目列表等顶层主列表复用:每行返回登录用户自己的角色数组,无角色则为空数组。 + */ +@Schema(description = "管理后台 - 当前登录用户在该对象担任的角色") +@Data +public class CurrentUserRoleVO { + + @Schema(description = "角色稳定标识(system_role.code)", requiredMode = Schema.RequiredMode.REQUIRED, example = "product_manager") + private String roleKey; + + @Schema(description = "角色中文名(system_role.name)", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品经理") + private String roleName; + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java index bf62cc3..28391a6 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/ProductController.java @@ -72,8 +72,8 @@ public class ProductController { @GetMapping("/page") @Operation(summary = "获取产品分页") public CommonResult> getProductPage(@Valid ProductPageReqVO pageReqVO) { - PageResult pageResult = productService.getProductPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, ProductRespVO.class)); + // VO 组装(含当前用户角色聚合)已下沉到 Service,Controller 直接返回 + return success(productService.getProductPage(pageReqVO)); } @GetMapping("/overview-summary") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductRespVO.java index 1c58cc1..8cda988 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/product/vo/product/ProductRespVO.java @@ -1,9 +1,11 @@ package com.njcn.rdms.module.project.controller.admin.product.vo.product; +import com.njcn.rdms.module.project.controller.admin.common.vo.CurrentUserRoleVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - 产品 Response VO") @Data @@ -39,4 +41,7 @@ public class ProductRespVO { @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime updateTime; + @Schema(description = "当前登录用户在该产品担任的角色(无角色则为空数组)") + private List currentUserRoles; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/project/ProjectRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/project/ProjectRespVO.java index 2803b17..9eeb8f8 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/project/ProjectRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/vo/project/ProjectRespVO.java @@ -1,11 +1,13 @@ package com.njcn.rdms.module.project.controller.admin.project.vo.project; +import com.njcn.rdms.module.project.controller.admin.common.vo.CurrentUserRoleVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - 项目 Response VO") @Data @@ -53,5 +55,7 @@ public class ProjectRespVO { private LocalDateTime createTime; @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime updateTime; + @Schema(description = "当前登录用户在该项目担任的角色(无角色则为空数组)") + private List currentUserRoles; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/member/UserObjectRoleMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/member/UserObjectRoleMapper.java index 3fdca3c..7467d84 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/member/UserObjectRoleMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/member/UserObjectRoleMapper.java @@ -125,4 +125,20 @@ public interface UserObjectRoleMapper extends BaseMapperX { .eq(UserObjectRoleDO::getStatus, 0)); } + /** + * 顶层列表「当前登录用户角色」用:查某用户在一批指定对象上的活跃角色行(status=0)。 + * 三条件精确命中,结果集只回当前页对象、与分页规模解耦,内存按 objectId 分组即可,无 N+1。 + */ + default List selectActiveListByObjectTypeAndUserIdAndObjectIds( + String objectType, Long userId, Collection objectIds) { + if (objectIds == null || objectIds.isEmpty()) { + return Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .eq(UserObjectRoleDO::getObjectType, objectType) + .eq(UserObjectRoleDO::getUserId, userId) + .in(UserObjectRoleDO::getObjectId, objectIds) + .eq(UserObjectRoleDO::getStatus, 0)); + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifySendEvent.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifySendEvent.java index a0988f4..a5a371b 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifySendEvent.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifySendEvent.java @@ -24,24 +24,33 @@ public class NotifySendEvent { private final Map params; /** 消息等级,见 {@link NotifyMessageLevelConstants}(默认普通) */ private final Integer level; + /** 操作人用户编号;非空时由监听器从接收人中排除(创建通知用),存量场景传 null 不排除 */ + private final Long operatorUserId; private NotifySendEvent(Collection userIds, String templateCode, - Map params, Integer level) { + Map params, Integer level, Long operatorUserId) { this.userIds = userIds; this.templateCode = templateCode; this.params = params; this.level = level; + this.operatorUserId = operatorUserId; } - /** 普通等级(兼容存量调用) */ + /** 普通等级、不排除操作人(兼容存量调用) */ public static NotifySendEvent of(Collection userIds, String templateCode, Map params) { - return new NotifySendEvent(userIds, templateCode, params, NotifyMessageLevelConstants.NORMAL); + return new NotifySendEvent(userIds, templateCode, params, NotifyMessageLevelConstants.NORMAL, null); } - /** 指定等级 */ + /** 指定等级、不排除操作人(兼容告警 / 工作报告催办) */ public static NotifySendEvent of(Collection userIds, String templateCode, Map params, Integer level) { - return new NotifySendEvent(userIds, templateCode, params, level); + return new NotifySendEvent(userIds, templateCode, params, level, null); + } + + /** 指定等级 + 排除操作人(对象创建通知用) */ + public static NotifySendEvent of(Collection userIds, String templateCode, + Map params, Integer level, Long operatorUserId) { + return new NotifySendEvent(userIds, templateCode, params, level, operatorUserId); } public Collection getUserIds() { @@ -60,4 +69,8 @@ public class NotifySendEvent { return level; } + public Long getOperatorUserId() { + return operatorUserId; + } + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifySendEventListener.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifySendEventListener.java index b49063f..5f0998c 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifySendEventListener.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifySendEventListener.java @@ -38,6 +38,10 @@ public class NotifySendEventListener { targets.add(userId); } } + // 排除操作人:创建通知不发给操作人自己(operatorUserId 为 null 的存量场景不排除) + if (event.getOperatorUserId() != null) { + targets.remove(event.getOperatorUserId()); + } if (targets.isEmpty()) { return; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java index c505b8f..a54db20 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java @@ -28,4 +28,19 @@ public class NotifyTemplateCodeConstants { /** 工作报告团队催办:主管催办下属提交指定周期工作报告 */ public static final String WORK_REPORT_TEAM_REMIND = "work_report_team_remind"; + /** 执行指派:创建执行后通知负责人 + 协办人 */ + public static final String EXECUTION_ASSIGNED = "execution_assigned"; + + /** 项目创建:通知项目团队成员(排除创建者角色) */ + public static final String PROJECT_CREATED = "project_created"; + + /** 产品创建:通知产品团队成员(排除创建者角色) */ + public static final String PRODUCT_CREATED = "product_created"; + + /** 项目需求创建:通知处理人 */ + public static final String PROJECT_REQUIREMENT_ASSIGNED = "project_requirement_assigned"; + + /** 产品需求创建:通知处理人 */ + public static final String PRODUCT_REQUIREMENT_ASSIGNED = "product_requirement_assigned"; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/TeamNotifyRecipientResolver.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/TeamNotifyRecipientResolver.java new file mode 100644 index 0000000..fff388a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/TeamNotifyRecipientResolver.java @@ -0,0 +1,71 @@ +package com.njcn.rdms.module.project.framework.notify; + +import com.njcn.rdms.module.project.constant.ObjectRoleConstants; +import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO; +import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper; +import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** 项目 / 产品团队通知接收人解析:仅 active、排除创建者角色、按 userId 取代表角色(经理优先)+ 中文名 */ +@Component +public class TeamNotifyRecipientResolver { + + @Resource + private UserObjectRoleMapper userObjectRoleMapper; + @Resource + private ObjectPermissionApi objectPermissionApi; + + public List resolveActiveExcludingCreator( + String objectType, Long objectId, String creatorRoleCode, String managerRoleCode) { + // 1. 取对象全部成员行,仅保留有效(active)成员 + List active = userObjectRoleMapper.selectListByObject(objectType, objectId).stream() + .filter(r -> ObjectRoleConstants.MEMBER_STATUS_ACTIVE.equals(r.getStatus())) + .collect(Collectors.toList()); + if (active.isEmpty()) { + return Collections.emptyList(); + } + // 2. 批量取角色摘要(含 code / 中文名),构建 roleId → 角色 映射 + Set roleIds = active.stream().map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet()); + List roles = objectPermissionApi + .getObjectRoleList(roleIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT, objectType) + .getCheckedData(); + if (roles == null || roles.isEmpty()) { + return Collections.emptyList(); + } + Map roleMap = roles.stream() + .collect(Collectors.toMap(ObjectRoleRespDTO::getId, Function.identity())); + + // 3. 逐行排除创建者角色行;同时有创建者+其它角色的用户仍按其它角色保留,纯创建者用户被剔除 + List kept = active.stream() + .filter(r -> { + ObjectRoleRespDTO role = roleMap.get(r.getRoleId()); + return role != null && !creatorRoleCode.equals(role.getCode()); + }) + .collect(Collectors.toList()); + + // 4. 按 userId 聚合,取代表角色:经理优先,否则 roleId 升序兜底;保持首次出现顺序 + Map> byUser = kept.stream() + .collect(Collectors.groupingBy(UserObjectRoleDO::getUserId, LinkedHashMap::new, Collectors.toList())); + List result = new ArrayList<>(); + byUser.forEach((userId, rows) -> { + UserObjectRoleDO primary = rows.stream() + .filter(r -> { + ObjectRoleRespDTO role = roleMap.get(r.getRoleId()); + return role != null && managerRoleCode.equals(role.getCode()); + }) + .findFirst() + .orElseGet(() -> rows.stream() + .min(Comparator.comparing(UserObjectRoleDO::getRoleId)) + .orElse(rows.get(0))); + ObjectRoleRespDTO role = roleMap.get(primary.getRoleId()); + result.add(new TeamRecipient(userId, role == null ? "" : role.getName())); + }); + return result; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/TeamRecipient.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/TeamRecipient.java new file mode 100644 index 0000000..a751548 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/TeamRecipient.java @@ -0,0 +1,4 @@ +package com.njcn.rdms.module.project.framework.notify; + +/** 团队通知接收人:用户编号 + 代表角色中文名 */ +public record TeamRecipient(Long userId, String roleName) {} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/member/CurrentUserRoleResolver.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/member/CurrentUserRoleResolver.java new file mode 100644 index 0000000..1804be9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/member/CurrentUserRoleResolver.java @@ -0,0 +1,96 @@ +package com.njcn.rdms.module.project.service.member; + +import com.njcn.rdms.module.project.constant.ObjectRoleConstants; +import com.njcn.rdms.module.project.controller.admin.common.vo.CurrentUserRoleVO; +import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO; +import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper; +import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 批量解析「当前登录用户在一批同类型对象上担任的角色」。 + * + *

供产品列表、项目列表等顶层主列表复用:对当前页只做两次额外查询 + *(角色行一次、角色名翻译一次),无 N+1。返回结果不做任何可见性过滤—— + * 创建者 / 隐式观察者等业务自动赋予角色也会返回(口径见 + * docs/superpowers/specs/2026-06-17-列表当前用户角色-design.md),因此不读 {@code visible}。 + */ +@Component +public class CurrentUserRoleResolver { + + @Resource + private UserObjectRoleMapper userObjectRoleMapper; + @Resource + private ObjectPermissionApi objectPermissionApi; + + /** + * @param objectType 对象类型(product / project) + * @param userId 当前登录用户编号 + * @param objectIds 当前页对象编号集合 + * @param managerRoleCode 该对象类型的经理角色编码,用于排序时把经理置顶 + * @return objectId -> 角色列表(稳定顺序:经理优先,其余按 roleId 升序); + * 无角色的对象不在返回 map 中,调用方按空数组处理 + */ + public Map> resolveByObjectIds( + String objectType, Long userId, Collection objectIds, String managerRoleCode) { + if (userId == null || objectIds == null || objectIds.isEmpty()) { + return Collections.emptyMap(); + } + List rows = userObjectRoleMapper + .selectActiveListByObjectTypeAndUserIdAndObjectIds(objectType, userId, objectIds); + if (rows.isEmpty()) { + return Collections.emptyMap(); + } + // 一次性翻译涉及的角色(roleId -> code/name),避免逐对象翻译 + Set roleIds = rows.stream().map(UserObjectRoleDO::getRoleId) + .filter(Objects::nonNull).collect(Collectors.toSet()); + Map roleMap = loadRoleMap(roleIds, objectType); + // 稳定排序:经理角色置顶,其余按 roleId 升序,避免每次返回顺序漂移 + Comparator order = Comparator + .comparingInt((ObjectRoleRespDTO d) -> Objects.equals(managerRoleCode, d.getCode()) ? 0 : 1) + .thenComparing(ObjectRoleRespDTO::getId, Comparator.nullsLast(Comparator.naturalOrder())); + return rows.stream() + .filter(r -> r.getObjectId() != null && roleMap.containsKey(r.getRoleId())) + .collect(Collectors.groupingBy(UserObjectRoleDO::getObjectId)) + .entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream() + .map(r -> roleMap.get(r.getRoleId())) + .sorted(order) + .map(this::toVO) + .collect(Collectors.toList()))); + } + + private CurrentUserRoleVO toVO(ObjectRoleRespDTO dto) { + CurrentUserRoleVO vo = new CurrentUserRoleVO(); + vo.setRoleKey(dto.getCode()); + vo.setRoleName(dto.getName()); + return vo; + } + + private Map loadRoleMap(Set roleIds, String objectType) { + if (roleIds.isEmpty()) { + return Collections.emptyMap(); + } + List roles = objectPermissionApi + .getObjectRoleList(roleIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT, objectType) + .getCheckedData(); + if (roles == null || roles.isEmpty()) { + return Collections.emptyMap(); + } + return roles.stream().collect(Collectors.toMap( + ObjectRoleRespDTO::getId, Function.identity(), (a, b) -> a)); + } + +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/member/ObjectRoleAutoAssignServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/member/ObjectRoleAutoAssignServiceImpl.java index 2254fad..40879a2 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/member/ObjectRoleAutoAssignServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/member/ObjectRoleAutoAssignServiceImpl.java @@ -44,14 +44,14 @@ public class ObjectRoleAutoAssignServiceImpl implements ObjectRoleAutoAssignServ Long creatorRoleId = resolveRoleId(creatorRoleCode, objectType); Long managerRoleId = resolveRoleId(managerRoleCode, objectType); - insertOrReactivate(objectType, objectId, managerUserId, managerRoleId, "auto: manager"); - insertOrReactivate(objectType, objectId, creatorUserId, creatorRoleId, "auto: creator"); + insertOrReactivate(objectType, objectId, managerUserId, managerRoleId); + insertOrReactivate(objectType, objectId, creatorUserId, creatorRoleId); } @Override public void assignCreator(String objectType, Long objectId, Long creatorUserId, String creatorRoleCode) { Long creatorRoleId = resolveRoleId(creatorRoleCode, objectType); - insertOrReactivate(objectType, objectId, creatorUserId, creatorRoleId, "auto: creator"); + insertOrReactivate(objectType, objectId, creatorUserId, creatorRoleId); } @Override @@ -64,7 +64,7 @@ public class ObjectRoleAutoAssignServiceImpl implements ObjectRoleAutoAssignServ watcherUserIds.stream() .filter(Objects::nonNull) .distinct() - .forEach(uid -> insertOrReactivate(objectType, objectId, uid, watcherRoleId, "auto: watcher")); + .forEach(uid -> insertOrReactivate(objectType, objectId, uid, watcherRoleId)); } private Long resolveRoleId(String roleCode, String objectType) { @@ -86,7 +86,7 @@ public class ObjectRoleAutoAssignServiceImpl implements ObjectRoleAutoAssignServ return role.getId(); } - private void insertOrReactivate(String objectType, Long objectId, Long userId, Long roleId, String remark) { + private void insertOrReactivate(String objectType, Long objectId, Long userId, Long roleId) { UserObjectRoleDO existing = userObjectRoleMapper .selectByObjectUserAndRole(objectType, objectId, userId, roleId); if (existing != null && Objects.equals(existing.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) { @@ -102,13 +102,15 @@ public class ObjectRoleAutoAssignServiceImpl implements ObjectRoleAutoAssignServ row.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE); row.setJoinedTime(now); row.setLeftTime(null); - row.setRemark(remark); + // 系统自动落的角色行不写备注,避免内部标记泄漏到团队成员列表的备注列 + row.setRemark(null); userObjectRoleMapper.insert(row); } else { existing.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE); existing.setLeftTime(null); existing.setJoinedTime(now); - existing.setRemark(remark); + // 复活时一并清掉历史脏备注(旧版本写过 "auto: xxx") + existing.setRemark(null); userObjectRoleMapper.updateById(existing); } } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java index 961938c..088176b 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImpl.java @@ -21,6 +21,8 @@ import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO; import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper; import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper; +import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO; +import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper; @@ -31,8 +33,12 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMappe import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver; import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator; +import com.njcn.rdms.module.project.framework.notify.NotifySendEvent; +import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants; import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission; +import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants; import jakarta.annotation.Resource; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -127,6 +133,10 @@ public class ProductRequirementServiceImpl implements ProductRequirementService private AttachmentFileIdResolver attachmentFileIdResolver; @Resource private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver; + @Resource + private ProductMapper productMapper; + @Resource + private ApplicationEventPublisher applicationEventPublisher; // ========== 需求增删改查 ========== @@ -170,9 +180,30 @@ public class ProductRequirementServiceImpl implements ProductRequirementService // 写入业务审计日志 writeBizAuditLog(requirement, ACTION_CREATE, null, initialStatus, buildRequirementFieldChanges(null, requirement), null); + // 创建后通知处理人 + publishRequirementAssignedNotify(requirement); return requirement.getId(); } + /** + * 创建产品需求后通知处理人(currentHandlerUserId);提出人不发,操作人由监听器统一排除。 + * 仅 publish,发送由 NotifySendEventListener 事务提交后处理;通知失败不影响需求创建。 + */ + @VisibleForTesting + void publishRequirementAssignedNotify(ProductRequirementDO requirement) { + Long handlerId = requirement.getCurrentHandlerUserId(); + if (handlerId == null) { + return; // 无处理人则不发 + } + Map params = new HashMap<>(); + ProductDO product = productMapper.selectById(requirement.getProductId()); + params.put("productName", product == null ? "" : product.getName()); + params.put("requirementTitle", requirement.getTitle()); + applicationEventPublisher.publishEvent(NotifySendEvent.of( + List.of(handlerId), NotifyTemplateCodeConstants.PRODUCT_REQUIREMENT_ASSIGNED, params, + NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId())); + } + @Override @Transactional(rollbackFor = Exception.class) @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#updateReqVO.productId", diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductService.java index 26adecc..4097369 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductService.java @@ -6,6 +6,7 @@ import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductC import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO; @@ -74,10 +75,12 @@ public interface ProductService { /** * 获取产品分页 * + *

每个列表项含「当前登录用户在该产品担任的角色」({@code currentUserRoles}),无角色为空数组。 + * * @param pageReqVO 分页请求 * @return 分页结果 */ - PageResult getProductPage(ProductPageReqVO pageReqVO); + PageResult getProductPage(ProductPageReqVO pageReqVO); /** * 获取产品入口页概览统计 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductServiceImpl.java index 94886f7..2ce687d 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductServiceImpl.java @@ -12,6 +12,7 @@ import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService; import com.njcn.rdms.module.project.constant.ObjectActivityConstants; import com.njcn.rdms.module.project.constant.ObjectRoleConstants; import com.njcn.rdms.module.project.constant.ProductObjectConstants; +import com.njcn.rdms.module.project.controller.admin.common.vo.CurrentUserRoleVO; import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextNavRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextProductRespVO; @@ -88,9 +89,15 @@ public class ProductServiceImpl implements ProductService { @Resource private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService; @Resource + private com.njcn.rdms.module.project.service.member.CurrentUserRoleResolver currentUserRoleResolver; + @Resource private ObjectDataScopeService objectDataScopeService; @Resource private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver; + @Resource + private org.springframework.context.ApplicationEventPublisher applicationEventPublisher; + @Resource + private com.njcn.rdms.module.project.framework.notify.TeamNotifyRecipientResolver teamNotifyRecipientResolver; @Override @Transactional(rollbackFor = Exception.class) @@ -116,6 +123,8 @@ public class ProductServiceImpl implements ProductService { initDefaultRequirementModule(product); writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_CREATE, null, initialStatus, buildProductFieldChanges(null, product), null); + // 团队成员已全部落库,发「产品创建」通知(操作人由监听器统一排除) + publishProductCreatedNotify(product); return product.getId(); } @@ -177,9 +186,37 @@ public class ProductServiceImpl implements ProductService { // 8) 产品创建审计 writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_CREATE, null, initialStatus, buildProductFieldChanges(null, product), null); + // 9) 团队成员已全部落库,发「产品创建」通知(操作人由监听器统一排除) + publishProductCreatedNotify(product); return product.getId(); } + /** + * 创建产品后逐成员发「产品创建」站内信:显式团队成员(排除创建者角色),每人带自己的角色名; + * 操作人自己由监听器统一排除。仅 publish,发送由 NotifySendEventListener 事务提交后处理。 + */ + @VisibleForTesting + void publishProductCreatedNotify(ProductDO product) { + Long operatorId = SecurityFrameworkUtils.getLoginUserId(); + List recipients = + teamNotifyRecipientResolver.resolveActiveExcludingCreator( + ProductObjectConstants.OBJECT_TYPE, product.getId(), + com.njcn.rdms.module.system.enums.permission.RoleCodeEnum.PRODUCT_CREATOR.getCode(), + ProductObjectConstants.MANAGER_ROLE_CODE); + for (com.njcn.rdms.module.project.framework.notify.TeamRecipient r : recipients) { + // 逐成员一条事件:每人带自己的代表角色中文名 + Map params = new HashMap<>(); + params.put("productName", product.getName()); + params.put("roleName", r.roleName()); + applicationEventPublisher.publishEvent( + com.njcn.rdms.module.project.framework.notify.NotifySendEvent.of( + java.util.List.of(r.userId()), + com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants.PRODUCT_CREATED, + params, + com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants.NORMAL, operatorId)); + } + } + /** * 校验初始团队成员列表。 * @@ -333,7 +370,7 @@ public class ProductServiceImpl implements ProductService { } @Override - public PageResult getProductPage(ProductPageReqVO pageReqVO) { + public PageResult getProductPage(ProductPageReqVO pageReqVO) { // 计算当前用户在 product 域的数据权限范围 Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE); @@ -374,7 +411,30 @@ public class ProductServiceImpl implements ProductService { } // ALL 状态不加任何 scope 条件,直接查全部 - return productMapper.selectPage(pageReqVO, wrapper); + PageResult doPage = productMapper.selectPage(pageReqVO, wrapper); + return convertProductListWithRoles(doPage); + } + + /** + * 产品分页 DO → RespVO,并回填「当前登录用户在该产品的角色」。 + * 角色聚合对当前页仅两次查询(角色行 + 角色名翻译),无 N+1;无角色的产品返回空数组。 + */ + private PageResult convertProductListWithRoles(PageResult doPage) { + List list = doPage.getList(); + if (list == null || list.isEmpty()) { + return new PageResult<>(new ArrayList<>(), doPage.getTotal()); + } + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + List objectIds = list.stream() + .map(ProductDO::getId).filter(Objects::nonNull).collect(Collectors.toList()); + Map> rolesByProduct = currentUserRoleResolver.resolveByObjectIds( + ProductObjectConstants.OBJECT_TYPE, loginUserId, objectIds, ProductObjectConstants.MANAGER_ROLE_CODE); + List voList = list.stream().map(p -> { + ProductRespVO vo = BeanUtils.toBean(p, ProductRespVO.class); + vo.setCurrentUserRoles(rolesByProduct.getOrDefault(p.getId(), Collections.emptyList())); + return vo; + }).collect(Collectors.toList()); + return new PageResult<>(voList, doPage.getTotal()); } @Override diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java index f308e9f..b564cc5 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java @@ -25,6 +25,7 @@ import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.Proj import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO; import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO; import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO; import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementModuleDO; import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementStatusLogDO; @@ -33,6 +34,7 @@ import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransition import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper; +import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementStatusLogMapper; @@ -42,9 +44,13 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMappe import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver; import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator; +import com.njcn.rdms.module.project.framework.notify.NotifySendEvent; +import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants; +import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants; import com.njcn.rdms.module.project.service.status.StatusActionTextResolver; import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission; import jakarta.annotation.Resource; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -135,6 +141,10 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService private ProjectExecutionMapper projectExecutionMapper; @Resource private StatusActionTextResolver statusActionTextResolver; + @Resource + private ProjectMapper projectMapper; + @Resource + private ApplicationEventPublisher applicationEventPublisher; @Override @Transactional(rollbackFor = Exception.class) @@ -171,9 +181,29 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService writeBizAuditLog(requirement, ACTION_CREATE, null, initialStatus, buildRequirementFieldChanges(null, requirement), null); + publishRequirementAssignedNotify(requirement); return requirement.getId(); } + /** + * 创建项目需求后通知处理人(currentHandlerUserId);提出人不发,操作人由监听器统一排除。 + * 仅 publish,发送由 NotifySendEventListener 事务提交后处理;通知失败不影响需求创建。 + */ + @VisibleForTesting + void publishRequirementAssignedNotify(ProjectRequirementDO requirement) { + Long handlerId = requirement.getCurrentHandlerUserId(); + if (handlerId == null) { + return; // 无处理人则不发 + } + Map params = new HashMap<>(); + ProjectDO project = projectMapper.selectById(requirement.getProjectId()); + params.put("projectName", project == null ? "" : project.getProjectName()); + params.put("requirementTitle", requirement.getTitle()); + applicationEventPublisher.publishEvent(NotifySendEvent.of( + List.of(handlerId), NotifyTemplateCodeConstants.PROJECT_REQUIREMENT_ASSIGNED, params, + NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId())); + } + @Override @Transactional(rollbackFor = Exception.class) @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#updateReqVO.projectId", diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectServiceImpl.java index c6de139..434d4c7 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectServiceImpl.java @@ -12,6 +12,7 @@ import com.njcn.rdms.module.project.constant.ProductObjectConstants; import com.njcn.rdms.module.project.constant.ProjectObjectConstants; import com.njcn.rdms.module.project.constant.ProjectExecutionConstants; import com.njcn.rdms.module.project.constant.ProjectRequirementConstants; +import com.njcn.rdms.module.project.controller.admin.common.vo.CurrentUserRoleVO; import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectContextNavRespVO; import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectContextProjectRespVO; @@ -127,9 +128,17 @@ class ProjectServiceImpl implements ProjectService { @Resource private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService; @Resource + private com.njcn.rdms.module.project.service.member.CurrentUserRoleResolver currentUserRoleResolver; + @Resource private ObjectDataScopeService objectDataScopeService; @Resource private StatusActionTextResolver statusActionTextResolver; + /** 站内信事件发布:创建项目后发「项目创建」通知,由 NotifySendEventListener 事务提交后统一发送。 */ + @Resource + private org.springframework.context.ApplicationEventPublisher applicationEventPublisher; + /** 团队通知接收人解析:仅 active、排除创建者角色、逐成员带代表角色中文名。 */ + @Resource + private com.njcn.rdms.module.project.framework.notify.TeamNotifyRecipientResolver teamNotifyRecipientResolver; @Override @Transactional(rollbackFor = Exception.class) @@ -167,6 +176,8 @@ class ProjectServiceImpl implements ProjectService { initDefaultRequirementModule(project); writeBizAuditLog(project, ObjectActivityConstants.PROJECT_ACTION_CREATE, null, initialStatus, buildProjectFieldChanges(null, project), null); + // 团队成员已全部落库,发「项目创建」通知(操作人由监听器统一排除) + publishProjectCreatedNotify(project); return project.getId(); } @@ -243,9 +254,37 @@ class ProjectServiceImpl implements ProjectService { // 9) 初始化项目需求的根模块 initDefaultRequirementModule(project); + // 10) 团队成员已全部落库,发「项目创建」通知(操作人由监听器统一排除) + publishProjectCreatedNotify(project); return project.getId(); } + /** + * 创建项目后逐成员发「项目创建」站内信:显式团队成员(排除创建者角色),每人带自己的角色名; + * 操作人自己由监听器统一排除。仅 publish,发送由 NotifySendEventListener 事务提交后处理。 + */ + @VisibleForTesting + void publishProjectCreatedNotify(ProjectDO project) { + Long operatorId = SecurityFrameworkUtils.getLoginUserId(); + List recipients = + teamNotifyRecipientResolver.resolveActiveExcludingCreator( + ProjectObjectConstants.OBJECT_TYPE, project.getId(), + com.njcn.rdms.module.system.enums.permission.RoleCodeEnum.PROJECT_CREATOR.getCode(), + ProjectObjectConstants.MANAGER_ROLE_CODE); + for (com.njcn.rdms.module.project.framework.notify.TeamRecipient r : recipients) { + // 逐成员一条事件:每人带自己的代表角色中文名 + Map params = new HashMap<>(); + params.put("projectName", project.getProjectName()); + params.put("roleName", r.roleName()); + applicationEventPublisher.publishEvent( + com.njcn.rdms.module.project.framework.notify.NotifySendEvent.of( + java.util.List.of(r.userId()), + com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants.PROJECT_CREATED, + params, + com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants.NORMAL, operatorId)); + } + } + /** * 新接口专用:严格的方向解析。 * @@ -903,12 +942,19 @@ class ProjectServiceImpl implements ProjectService { Map userMap = userIds.isEmpty() ? Collections.emptyMap() : adminUserApi.getUserMap(userIds); + // 当前登录用户在这批项目上的角色(当前页两次查询,无 N+1;无角色项目返回空数组) + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + List objectIds = projects.stream() + .map(ProjectDO::getId).filter(Objects::nonNull).collect(Collectors.toList()); + Map> rolesByProject = currentUserRoleResolver.resolveByObjectIds( + ProjectObjectConstants.OBJECT_TYPE, loginUserId, objectIds, ProjectObjectConstants.MANAGER_ROLE_CODE); return projects.stream().map(project -> { ProjectRespVO respVO = BeanUtils.toBean(project, ProjectRespVO.class); ProductDO product = project.getProductId() == null ? null : productMap.get(project.getProductId()); respVO.setProductName(product == null ? null : product.getName()); AdminUserRespDTO manager = project.getManagerUserId() == null ? null : userMap.get(project.getManagerUserId()); respVO.setManagerUserNickname(manager == null ? null : manager.getNickname()); + respVO.setCurrentUserRoles(rolesByProject.getOrDefault(project.getId(), Collections.emptyList())); return respVO; }).collect(Collectors.toList()); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java index fb693ce..868a814 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImpl.java @@ -43,6 +43,8 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; +import com.njcn.rdms.module.project.framework.notify.NotifySendEvent; +import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants; import com.njcn.rdms.module.project.util.DueRangeSupport; import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission; import com.njcn.rdms.module.project.service.project.ProjectRequirementService; @@ -52,8 +54,10 @@ import com.njcn.rdms.module.project.service.status.StatusActionTextResolver; import com.njcn.rdms.module.system.api.dict.DictDataApi; import com.njcn.rdms.module.system.api.user.AdminUserApi; import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; +import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -63,6 +67,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -123,6 +128,9 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { private ProjectRequirementMapper projectRequirementMapper; @Resource private StatusActionTextResolver statusActionTextResolver; + /** 站内信事件发布:创建执行后发「执行指派」通知,由 NotifySendEventListener 事务提交后统一发送。 */ + @Resource + private ApplicationEventPublisher applicationEventPublisher; /** * 任务服务:执行 cancel / pause / resume 时级联调任务侧 internal 入口。 * 与 ProjectTaskService 互相依赖(任务侧已注入 ProjectExecutionService 用于通知执行),用 @Lazy 打破循环。 @@ -167,9 +175,29 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService { writeExecutionAuditLog(execution, ObjectActivityConstants.EXECUTION_ACTION_CREATE, null, initialStatusCode, buildExecutionFieldChanges(null, execution), null); createExecutionAssignees(execution.getId(), projectId, assigneeUserIds); + publishExecutionAssignedNotify(execution); return execution.getId(); } + /** + * 创建执行后发送「执行指派」站内信事件:接收人 = 负责人 + 活跃协办人(操作人自己由监听器统一排除)。 + * 仅 publish 事件,发送由 NotifySendEventListener 事务提交后处理;通知失败不影响执行创建。 + */ + @VisibleForTesting + void publishExecutionAssignedNotify(ProjectExecutionDO execution) { + List recipients = new ArrayList<>(); + recipients.add(execution.getOwnerId()); + executionAssigneeMapper.selectActiveListByExecutionId(execution.getId()) + .forEach(a -> recipients.add(a.getUserId())); + Map params = new HashMap<>(); + ProjectDO project = projectMapper.selectById(execution.getProjectId()); + params.put("projectName", project == null ? "" : project.getProjectName()); + params.put("executionName", execution.getExecutionName()); + applicationEventPublisher.publishEvent(NotifySendEvent.of(recipients, + NotifyTemplateCodeConstants.EXECUTION_ASSIGNED, params, + NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId())); + } + @Override @Transactional(rollbackFor = Exception.class) @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskService.java index 9d2a368..25db686 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskService.java @@ -9,6 +9,8 @@ import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTask import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusActionReqVO; import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO; +import java.time.LocalDate; + /** * 项目任务 Service 接口。 */ @@ -98,6 +100,18 @@ public interface ProjectTaskService { */ void internalAutoStartByWorklog(ProjectTaskDO task); + /** + * 工作日志填报触发:当任务"实际开始时间"为空时,用给定的开始日期回填,使"任务何时开始" + * 以最早一条工作日志的开始日期为准。owner / 协办人共用,不限任务当前状态。 + *

+ * 与 {@link #internalAutoStartByWorklog} 解耦:本方法只回填 actualStartDate(不动实际结束日期、 + * 不推进任务状态)。调用顺序应在 internalAutoStartByWorklog 之前,确保后者的回填钩子见到非空值即跳过, + * 不会再用"提交当天"覆盖工作日志的开始日期。 + *

+ * 任务实际开始时间已有值、或入参为空时静默 return。 + */ + void fillActualStartDateIfAbsent(Long taskId, LocalDate startDate); + /** * 由执行 cancel 触发的级联:取消指定执行下所有未终态的顶层任务(parentTaskId IS NULL)。 * 顶层任务自身的内部链路会再级联自己的子任务,整棵子树通过链式实现。 diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java index d36c253..9f4fd5b 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImpl.java @@ -53,6 +53,7 @@ import com.njcn.rdms.module.project.service.status.StatusActionTextResolver; import com.google.common.annotations.VisibleForTesting; import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; import com.njcn.rdms.module.system.api.dict.DictDataApi; +import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants; import com.njcn.rdms.module.system.api.user.AdminUserApi; import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; @@ -195,7 +196,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { } /** - * 创建任务后发送「任务指派」站内信事件:接收人 = 负责人 + 活跃协办人(含操作人自己)。 + * 创建任务后发送「任务指派」站内信事件:接收人 = 负责人 + 活跃协办人(操作人自己由监听器统一排除)。 * 仅 publish 事件,真正发送由 {@link com.njcn.rdms.module.project.framework.notify.NotifySendEventListener} * 在事务提交后处理;通知失败不影响任务创建。 */ @@ -209,8 +210,9 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { ProjectDO project = projectMapper.selectById(projectId); params.put("projectName", project == null ? "" : project.getProjectName()); params.put("taskName", task.getTaskTitle()); - applicationEventPublisher.publishEvent( - NotifySendEvent.of(recipients, NotifyTemplateCodeConstants.TASK_ASSIGNED, params)); + applicationEventPublisher.publishEvent(NotifySendEvent.of(recipients, + NotifyTemplateCodeConstants.TASK_ASSIGNED, params, + NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId())); } @Override @@ -451,6 +453,21 @@ public class ProjectTaskServiceImpl implements ProjectTaskService { current.getExecutionId(), current.getId(), fromStatus, toStatus, actionCode, reason); } + @Override + @Transactional(rollbackFor = Exception.class) + public void fillActualStartDateIfAbsent(Long taskId, LocalDate startDate) { + if (taskId == null || startDate == null) { + return; + } + ProjectTaskDO current = projectTaskMapper.selectById(taskId); + // 仅在实际开始时间为空时回填;已有值(含 internalAutoStartByWorklog 已写过)则保持不变 + if (current == null || current.getActualStartDate() != null) { + return; + } + // 只动开始日期:实际结束日期传 null,依据全局 FieldStrategy 不会被覆盖 + projectTaskMapper.updateActualDatesById(taskId, startDate, null); + } + @Override public ProjectTaskDO getTask(Long projectId, Long executionId, Long taskId) { validateExecutionExists(projectId, executionId); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java index e7333a7..108ccbe 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/task/worklog/TaskWorklogServiceImpl.java @@ -118,7 +118,11 @@ public class TaskWorklogServiceImpl implements TaskWorklogService { TaskWorklogDO worklog = buildWorklog(taskId, loginUserId, reqVO); taskWorklogMapper.insert(worklog); - // 任意填报人(owner / 协办人)都触发任务自动开始(仅当任务仍处初始态时生效),并写入 actualStartDate。 + // 实际开始时间为空时,用本条工作日志的开始日期回填(owner / 协办人一致,不限任务当前状态)。 + // 必须在 internalAutoStartByWorklog 之前:先把开始日期落库,自动开始里的回填钩子见到非空值即跳过, + // 不会再用"提交当天"覆盖工作日志填写的开始日期。 + projectTaskService.fillActualStartDateIfAbsent(taskId, reqVO.getStartDate()); + // 任意填报人(owner / 协办人)都触发任务自动开始(仅当任务仍处初始态时生效),推进任务状态。 // 任务"是否开始"是客观事实,协办人开工同样代表任务已开始,不应等 owner 补工时才反映。 projectTaskService.internalAutoStartByWorklog(task); // 仅 owner 填报触发任务进度同步:任务整体进度以 owner 本人最新一条工时为权威源, diff --git a/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml b/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml deleted file mode 100644 index f89b278..0000000 --- a/rdms-project/rdms-project-boot/src/main/resources/application-dev.yaml +++ /dev/null @@ -1,92 +0,0 @@ -#################### 注册中心 + 配置中心相关配置 #################### - -spring: - cloud: - nacos: - server-addr: 192.168.1.103:18848 # Nacos 服务器地址 - username: # Nacos 账号 - password: # Nacos 密码 - discovery: # 【配置中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - metadata: - version: 1.0.0 # 服务实例的版本号,可用于灰度发布 - config: # 【注册中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - -#################### 数据库相关配置 #################### - # 数据源配置项 - autoconfigure: - exclude: - datasource: - druid: # Druid 【监控】相关的全局配置 - web-stat-filter: - enabled: true - stat-view-servlet: - enabled: true - allow: # 设置白名单,不填则允许所有访问 - url-pattern: /druid/* - login-username: # 控制台管理用户名和密码 - login-password: - filter: - stat: - enabled: true - log-slow-sql: true # 慢 SQL 记录 - slow-sql-millis: 100 - merge-sql: true - wall: - config: - multi-statement-allow: true - dynamic: # 多数据源配置 - druid: # Druid 【连接池】相关的全局配置 - initial-size: 5 # 初始连接数 - min-idle: 10 # 最小连接池数量 - max-active: 20 # 最大连接池数量 - max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) - min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) - max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) - validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 - test-while-idle: true - test-on-borrow: false - test-on-return: false - pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 - max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 - primary: master - datasource: - master: - url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 - username: root - password: njcnpqs - - # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 - data: - redis: - host: 192.168.1.22 # 地址 - port: 16379 # 端口 - database: 1 # 数据库索引 -# password: njcnpqs # 密码,建议生产环境开启 - - -#################### 监控相关配置 #################### - -# Actuator 监控端点的配置项 -management: - endpoints: - web: - base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator - exposure: - include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 *,可以开放所有端点。 - - -# 日志文件配置 -logging: - file: - name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 - -#################### RDMS 相关配置 #################### - -# RDMS 配置项,设置当前项目所有自定义的配置 -rdms: - demo: true # 开启演示模式 diff --git a/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml b/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml deleted file mode 100644 index f47cbf8..0000000 --- a/rdms-project/rdms-project-boot/src/main/resources/application-local.yaml +++ /dev/null @@ -1,98 +0,0 @@ -#################### 注册中心 + 配置中心相关配置 #################### -spring: - cloud: - nacos: - server-addr: 192.168.1.103:18848 # Nacos 服务器地址 - username: # Nacos 账号 - password: # Nacos 密码 - discovery: # 【配置中心】配置项 - namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - metadata: - version: 1.0.0 # 服务实例的版本号,可用于灰度发布 - config: # 【注册中心】配置项 - namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - -#################### 数据库相关配置 #################### - # 数据源配置项 - autoconfigure: - exclude: - datasource: - druid: # Druid 【监控】相关的全局配置 - web-stat-filter: - enabled: true - stat-view-servlet: - enabled: true - allow: # 设置白名单,不填则允许所有访问 - url-pattern: /druid/* - login-username: # 控制台管理用户名和密码 - login-password: - filter: - stat: - enabled: true - log-slow-sql: true # 慢 SQL 记录 - slow-sql-millis: 100 - merge-sql: true - wall: - config: - multi-statement-allow: true - dynamic: # 多数据源配置 - druid: # Druid 【连接池】相关的全局配置 - initial-size: 5 # 初始连接数 - min-idle: 10 # 最小连接池数量 - max-active: 20 # 最大连接池数量 - max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) - min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) - max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) - validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 - test-while-idle: true - test-on-borrow: false - test-on-return: false - pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 - max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 - primary: master - datasource: - master: - url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 - username: root - password: njcnpqs - - # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 - data: - redis: - host: 127.0.0.1 # 地址 - port: 6379 # 端口 - database: 1 # 数据库索引 -# password: njcnpqs # 密码,建议生产环境开启 - - -#################### 监控相关配置 #################### - -# Actuator 监控端点的配置项 -management: - endpoints: - web: - base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator - exposure: - include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 *,可以开放所有端点。 - - -# 日志文件配置 -logging: - level: - # 配置本模块 MyBatis Mapper 打印日志 - com.njcn.rdms.module.project.dal.mysql: debug - org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR - -# RDMS 配置项,设置当前项目所有自定义的本地扩展配置 -rdms: - env: # 多环境的配置项 - tag: ${HOSTNAME} - captcha: - enable: false - security: - mock-enable: true - access-log: # 访问日志的配置项 - enable: true diff --git a/rdms-project/rdms-project-boot/src/main/resources/application.yaml b/rdms-project/rdms-project-boot/src/main/resources/application.yaml index 00b477e..d1fa3e1 100644 --- a/rdms-project/rdms-project-boot/src/main/resources/application.yaml +++ b/rdms-project/rdms-project-boot/src/main/resources/application.yaml @@ -1,15 +1,27 @@ spring: application: name: rdms-project-server - profiles: - active: local main: allow-circular-references: true # 允许循环依赖,因为项目当前沿用三层架构组织方式。 allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如 Feign 等会存在重复定义的服务 + cloud: + # 注册中心 + 配置中心连接(值由根 pom 的 nacos.* 属性在打包时注入) + nacos: + server-addr: @nacos.server-addr@ + username: @nacos.username@ + password: @nacos.password@ + discovery: + namespace: @nacos.namespace@ + group: @nacos.group@ + metadata: + version: ${rdms.info.version} # 灰度发布用的实例版本号 + config: + namespace: @nacos.namespace@ + group: @nacos.group@ config: import: - - optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置 - - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 + - nacos:rdms-common.yaml # 公共非敏感配置(数据库地址/Redis/开关等) + - nacos:rdms-common-secret.yaml # 公共敏感配置(数据库账密/加解密秘钥) # Servlet 配置 servlet: # 文件上传相关配置项 @@ -48,6 +60,8 @@ server: logging: file: name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + level: + com.njcn.rdms.module.project.dal.mysql: debug # 打印本模块 Mapper 的 SQL 日志 --- #################### 接口文档配置 #################### springdoc: @@ -75,8 +89,7 @@ mybatis-plus: logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) banner: false # 关闭控制台的 Banner 打印 type-aliases-package: ${rdms.info.base-package}.dal.dataobject - encryptor: - password: cDHvwsYb9eyLNBHp # 加解密秘钥,生产环境务必通过 Nacos 注入,切勿硬编码 + # encryptor.password(@EncryptField 字段加解密秘钥)已外置到 Nacos rdms-common-secret.yaml,不再硬编码进 git mybatis-plus-join: banner: false # 关闭控制台的 Banner 打印 diff --git a/rdms-project/rdms-project-boot/src/main/resources/sql/due_alert_record.sql b/rdms-project/rdms-project-boot/src/main/resources/sql/due_alert_record.sql deleted file mode 100644 index 178e02c..0000000 --- a/rdms-project/rdms-project-boot/src/main/resources/sql/due_alert_record.sql +++ /dev/null @@ -1,19 +0,0 @@ --- 临期/逾期告警发送记录表(去重凭证) --- 设计见 docs/domains/project/2026-06-12-临期逾期告警-设计.html --- 临期档:同(object_type,object_id,planned_end_date快照)只发一次,改期=新快照=重新告警 --- 逾期档:按(object_type,object_id,alert_date)每天一条 -CREATE TABLE IF NOT EXISTS rdms_due_alert_record ( - id BIGINT NOT NULL COMMENT '主键(Java 侧 MyBatis-Plus 雪花生成,无 AUTO_INCREMENT)', - object_type VARCHAR(32) NOT NULL COMMENT '告警域对象类型:project/project_requirement/product_requirement/execution/task/personal_item', - object_id BIGINT NOT NULL COMMENT '对象主键', - alert_type VARCHAR(16) NOT NULL COMMENT '告警档:approaching=临期/overdue=逾期', - planned_end_date DATE NOT NULL COMMENT '判定时计划完成日快照(临期档去重键)', - alert_date DATE NOT NULL COMMENT '告警发送日(逾期档每天一条的去重键)', - creator VARCHAR(64) DEFAULT '' COMMENT '创建者', - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - updater VARCHAR(64) DEFAULT '' COMMENT '更新者', - update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - deleted BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', - PRIMARY KEY (id), - UNIQUE KEY uk_due_alert (object_type, object_id, alert_type, planned_end_date, alert_date) -) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '临期/逾期告警发送记录(去重凭证,只增不删)'; diff --git a/rdms-project/rdms-project-boot/src/main/resources/sql/td015_project_completion_index.sql b/rdms-project/rdms-project-boot/src/main/resources/sql/td015_project_completion_index.sql deleted file mode 100644 index e00c2e9..0000000 --- a/rdms-project/rdms-project-boot/src/main/resources/sql/td015_project_completion_index.sql +++ /dev/null @@ -1,24 +0,0 @@ --- TD-015 项目完成校验:按 project_id 数非终态子对象需要的索引 --- MySQL 无 CREATE INDEX IF NOT EXISTS,用 information_schema 守卫实现幂等(可重复执行);与演示库补丁 docs/sql/patches/2026-06-05-TD015-完成校验-01.sql 写法一致 - --- 需求表:原 idx_rdms_project_requirement_project_status_deleted 实际列为 (status_code, deleted),缺 project_id 前导,补齐 -SET @idx := (SELECT COUNT(*) FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'rdms_project_requirement' - AND INDEX_NAME = 'idx_rdms_project_requirement_proj_status'); -SET @sql := IF(@idx = 0, - 'CREATE INDEX idx_rdms_project_requirement_proj_status ON rdms_project_requirement (project_id, status_code, deleted)', - 'SELECT 1'); -PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; - --- 任务表(rdms_task):与执行表 idx_rdms_pe_proj_status 对齐,让按项目数非终态任务走覆盖前缀 -SET @idx := (SELECT COUNT(*) FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'rdms_task' - AND INDEX_NAME = 'idx_rdms_task_proj_status'); -SET @sql := IF(@idx = 0, - 'CREATE INDEX idx_rdms_task_proj_status ON rdms_task (project_id, status_code, deleted)', - 'SELECT 1'); -PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; - --- 执行表 rdms_project_execution 已有 idx_rdms_pe_proj_status(project_id, status_code, deleted),无需新增 diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/notify/NotifySendEventListenerTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/notify/NotifySendEventListenerTest.java index 55f1b36..d1fe9a1 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/notify/NotifySendEventListenerTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/notify/NotifySendEventListenerTest.java @@ -50,4 +50,17 @@ class NotifySendEventListenerTest extends BaseMockitoUnitTest { listener.onNotifySend(NotifySendEvent.of(Arrays.asList(1L, 2L), "task_assigned", new HashMap<>())); verify(notifyMessageSendApi, times(1)).sendSingleNotifyToAdmin(eq(2L), any(), any(), any()); } + + @Test + void testOnNotifySend_excludesOperator() { + // userIds 含操作人 1L 与 2L;operatorUserId=1L + listener.onNotifySend(NotifySendEvent.of( + Arrays.asList(1L, 2L), "task_assigned", new HashMap<>(), + NotifyMessageLevelConstants.NORMAL, 1L)); + // 1L 是操作人,应被排除;仅 2L 收到,且五参工厂的 level 应原样透传 + verify(notifyMessageSendApi, times(0)) + .sendSingleNotifyToAdmin(eq(1L), any(), any(), any()); + verify(notifyMessageSendApi, times(1)) + .sendSingleNotifyToAdmin(eq(2L), eq("task_assigned"), any(), eq(NotifyMessageLevelConstants.NORMAL)); + } } diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/notify/TeamNotifyRecipientResolverTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/notify/TeamNotifyRecipientResolverTest.java new file mode 100644 index 0000000..a0132eb --- /dev/null +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/framework/notify/TeamNotifyRecipientResolverTest.java @@ -0,0 +1,126 @@ +package com.njcn.rdms.module.project.framework.notify; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest; +import com.njcn.rdms.module.project.constant.ObjectRoleConstants; +import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO; +import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper; +import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +class TeamNotifyRecipientResolverTest extends BaseMockitoUnitTest { + + @InjectMocks + private TeamNotifyRecipientResolver resolver; + @Mock + private UserObjectRoleMapper userObjectRoleMapper; + @Mock + private ObjectPermissionApi objectPermissionApi; + + @Test + void resolve_filtersInactiveAndCreator_picksManagerAsPrimary() { + // u1: manager(active) + creator(active) → 保留,代表角色=manager + // u2: creator only(active) → 排除(纯创建者) + // u3: watcher(inactive) → 排除(失效) + UserObjectRoleDO r1 = row(1L, 100L, 0); // manager role id 100 + UserObjectRoleDO r1c = row(1L, 300L, 0); // creator role id 300 + UserObjectRoleDO r2 = row(2L, 300L, 0); // creator only + UserObjectRoleDO r3 = row(3L, 200L, 1); // watcher inactive + when(userObjectRoleMapper.selectListByObject("project", 10L)) + .thenReturn(List.of(r1, r1c, r2, r3)); + when(objectPermissionApi.getObjectRoleList(anySet(), eq(ObjectRoleConstants.ROLE_SCOPE_OBJECT), eq("project"))) + .thenReturn(CommonResult.success(List.of( + role(100L, "project_manager", "项目经理"), + role(200L, "project_watcher", "项目观察者"), + role(300L, "project_creator", "项目创建者")))); + + List out = resolver.resolveActiveExcludingCreator( + "project", 10L, "project_creator", "project_manager"); + + assertEquals(1, out.size()); + assertEquals(1L, out.get(0).userId()); + assertEquals("项目经理", out.get(0).roleName()); + } + + @Test + void resolve_noActiveMembers_returnsEmpty() { + // 全部失效 → 直接空集,不触达 RPC + UserObjectRoleDO r1 = row(1L, 100L, 1); + UserObjectRoleDO r2 = row(2L, 200L, 1); + when(userObjectRoleMapper.selectListByObject("product", 20L)) + .thenReturn(List.of(r1, r2)); + + List out = resolver.resolveActiveExcludingCreator( + "product", 20L, "product_creator", "product_manager"); + + assertTrue(out.isEmpty()); + } + + @Test + void resolve_mixedActiveInactiveSameUser_inactiveRowIgnored() { + // u1 同时持有 manager(active) 与 watcher(inactive):失效行既不应踢掉 u1,也不应成为代表角色 + UserObjectRoleDO r1 = row(1L, 100L, 0); // manager active + UserObjectRoleDO r1w = row(1L, 200L, 1); // watcher inactive + when(userObjectRoleMapper.selectListByObject("project", 10L)) + .thenReturn(List.of(r1, r1w)); + when(objectPermissionApi.getObjectRoleList(anySet(), eq(ObjectRoleConstants.ROLE_SCOPE_OBJECT), eq("project"))) + .thenReturn(CommonResult.success(List.of( + role(100L, "project_manager", "项目经理"), + role(200L, "project_watcher", "项目观察者")))); + + List out = resolver.resolveActiveExcludingCreator( + "project", 10L, "project_creator", "project_manager"); + + assertEquals(1, out.size()); + assertEquals(1L, out.get(0).userId()); + assertEquals("项目经理", out.get(0).roleName()); + } + + @Test + void resolve_nonManagerUser_fallsBackToLowestRoleId() { + // u1 持两个 active 非经理非创建者角色(200 与 400):代表角色按 roleId 升序取 200 + UserObjectRoleDO r1a = row(1L, 400L, 0); + UserObjectRoleDO r1b = row(1L, 200L, 0); + when(userObjectRoleMapper.selectListByObject("project", 10L)) + .thenReturn(List.of(r1a, r1b)); + when(objectPermissionApi.getObjectRoleList(anySet(), eq(ObjectRoleConstants.ROLE_SCOPE_OBJECT), eq("project"))) + .thenReturn(CommonResult.success(List.of( + role(200L, "project_watcher", "项目观察者"), + role(400L, "project_member", "项目成员")))); + + List out = resolver.resolveActiveExcludingCreator( + "project", 10L, "project_creator", "project_manager"); + + assertEquals(1, out.size()); + assertEquals(1L, out.get(0).userId()); + assertEquals("项目观察者", out.get(0).roleName()); + } + + private UserObjectRoleDO row(Long userId, Long roleId, Integer status) { + UserObjectRoleDO row = new UserObjectRoleDO(); + row.setUserId(userId); + row.setRoleId(roleId); + row.setStatus(status); + return row; + } + + private ObjectRoleRespDTO role(Long id, String code, String name) { + ObjectRoleRespDTO role = new ObjectRoleRespDTO(); + role.setId(id); + role.setCode(code); + role.setName(name); + return role; + } + +} diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java index e03dd70..b1c5617 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java @@ -53,6 +53,57 @@ class ProductRequirementServiceImplTest extends BaseMockitoUnitTest { private ObjectStatusModelMapper statusModelMapper; @Mock private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver; + @Mock + private com.njcn.rdms.module.project.dal.mysql.product.ProductMapper productMapper; + @Mock + private org.springframework.context.ApplicationEventPublisher applicationEventPublisher; + + // ========== 创建需求通知处理人测试(Task 9) ========== + + /** + * Task 9:创建产品需求后通知处理人(currentHandlerUserId),操作人由监听器统一排除。 + */ + @Test + void createRequirement_notifiesHandler() { + try (MockedStatic mocked = mockStatic(SecurityFrameworkUtils.class)) { + mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(9L); + ProductRequirementDO req = new ProductRequirementDO(); + req.setId(1L); + req.setProductId(20L); + req.setTitle("电池告警"); + req.setCurrentHandlerUserId(5L); + com.njcn.rdms.module.project.dal.dataobject.product.ProductDO product = + new com.njcn.rdms.module.project.dal.dataobject.product.ProductDO(); + product.setName("智能终端"); + when(productMapper.selectById(20L)).thenReturn(product); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.njcn.rdms.module.project.framework.notify.NotifySendEvent.class); + requirementService.publishRequirementAssignedNotify(req); + verify(applicationEventPublisher).publishEvent(captor.capture()); + com.njcn.rdms.module.project.framework.notify.NotifySendEvent ev = captor.getValue(); + assertEquals("product_requirement_assigned", ev.getTemplateCode()); + assertEquals(9L, ev.getOperatorUserId()); + assertTrue(ev.getUserIds().contains(5L)); + assertEquals("电池告警", ev.getParams().get("requirementTitle")); + assertEquals("智能终端", ev.getParams().get("productName")); + } + } + + /** + * Task 9:处理人为空时不发通知。 + */ + @Test + void createRequirement_whenHandlerNull_shouldNotPublish() { + ProductRequirementDO req = new ProductRequirementDO(); + req.setId(1L); + req.setProductId(20L); + req.setTitle("电池告警"); + req.setCurrentHandlerUserId(null); + + requirementService.publishRequirementAssignedNotify(req); + verify(applicationEventPublisher, never()).publishEvent(any()); + } // ========== 创建需求测试 ========== diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductServiceImplTest.java index 140d87b..fc2e1ea 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductServiceImplTest.java @@ -10,6 +10,7 @@ import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductC import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO; +import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductRespVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO; import com.njcn.rdms.module.project.service.datascope.ObjectDataScope; @@ -51,7 +52,10 @@ 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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -83,9 +87,15 @@ class ProductServiceImplTest extends BaseMockitoUnitTest { @Mock private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService; @Mock + private com.njcn.rdms.module.project.service.member.CurrentUserRoleResolver currentUserRoleResolver; + @Mock private ObjectDataScopeService objectDataScopeService; @Mock private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver; + @Mock + private com.njcn.rdms.module.project.framework.notify.TeamNotifyRecipientResolver teamNotifyRecipientResolver; + @Mock + private org.springframework.context.ApplicationEventPublisher applicationEventPublisher; @Test void createProduct_shouldCreateDefaultRequirementModule() { @@ -572,7 +582,7 @@ class ProductServiceImplTest extends BaseMockitoUnitTest { mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1L); when(objectDataScopeService.compute(1L, "product")).thenReturn(ObjectDataScope.empty()); - PageResult result = productService.getProductPage(new ProductPageReqVO()); + PageResult result = productService.getProductPage(new ProductPageReqVO()); assertThat(result.getList()).isEmpty(); verify(productMapper, never()).selectPage(any(ProductPageReqVO.class), any()); @@ -606,6 +616,31 @@ class ProductServiceImplTest extends BaseMockitoUnitTest { } } + @Test + void publishProductCreatedNotify_perMemberWithRole() { + try (MockedStatic mocked = mockStatic(SecurityFrameworkUtils.class)) { + mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(9L); + ProductDO product = new ProductDO(); + product.setId(20L); + product.setName("智能终端"); + when(teamNotifyRecipientResolver.resolveActiveExcludingCreator( + eq(com.njcn.rdms.module.project.constant.ProductObjectConstants.OBJECT_TYPE), eq(20L), + anyString(), anyString())) + .thenReturn(List.of(new com.njcn.rdms.module.project.framework.notify.TeamRecipient(5L, "产品经理"))); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.njcn.rdms.module.project.framework.notify.NotifySendEvent.class); + productService.publishProductCreatedNotify(product); + verify(applicationEventPublisher).publishEvent(captor.capture()); + com.njcn.rdms.module.project.framework.notify.NotifySendEvent ev = captor.getValue(); + assertEquals("product_created", ev.getTemplateCode()); + assertEquals(9L, ev.getOperatorUserId()); + assertEquals("智能终端", ev.getParams().get("productName")); + assertEquals("产品经理", ev.getParams().get("roleName")); + assertTrue(ev.getUserIds().contains(5L)); + } + } + private ProductDO createProduct(Long id, String directionCode, String name, Long managerUserId, String description, String statusCode) { ProductDO product = new ProductDO(); diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImplTest.java index 89c983c..d79cca8 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImplTest.java @@ -1,12 +1,15 @@ package com.njcn.rdms.module.project.service.project; import com.njcn.rdms.framework.common.exception.ServiceException; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest; import com.njcn.rdms.module.project.constant.ProjectExecutionConstants; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO; import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper; import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper; +import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper; import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementStatusLogMapper; @@ -15,9 +18,13 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver; +import com.njcn.rdms.module.project.framework.notify.NotifySendEvent; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.springframework.context.ApplicationEventPublisher; import java.math.BigDecimal; import java.util.HashMap; @@ -27,7 +34,11 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @@ -60,6 +71,10 @@ class ProjectRequirementServiceImplTest extends BaseMockitoUnitTest { private ProjectExecutionMapper projectExecutionMapper; @Mock private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver; + @Mock + private ProjectMapper projectMapper; + @Mock + private ApplicationEventPublisher applicationEventPublisher; @Test void validateUsableForExecution_whenRequirementIdIsNull_shouldDoNothing() { @@ -121,6 +136,49 @@ class ProjectRequirementServiceImplTest extends BaseMockitoUnitTest { assertEquals(ErrorCodeConstants.PROJECT_REQUIREMENT_HAS_EXECUTIONS_NOT_ALLOW_DELETE.getCode(), ex.getCode()); } + /** + * Task 8:创建项目需求后通知处理人(currentHandlerUserId),操作人由监听器统一排除。 + */ + @Test + void createRequirement_notifiesHandler() { + try (MockedStatic mocked = mockStatic(SecurityFrameworkUtils.class)) { + mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(9L); + ProjectRequirementDO req = new ProjectRequirementDO(); + req.setId(1L); + req.setProjectId(10L); + req.setTitle("登录改造"); + req.setCurrentHandlerUserId(5L); + ProjectDO project = new ProjectDO(); + project.setProjectName("灿能项目"); + when(projectMapper.selectById(10L)).thenReturn(project); + + ArgumentCaptor captor = ArgumentCaptor.forClass(NotifySendEvent.class); + projectRequirementService.publishRequirementAssignedNotify(req); + verify(applicationEventPublisher).publishEvent(captor.capture()); + NotifySendEvent ev = captor.getValue(); + assertEquals("project_requirement_assigned", ev.getTemplateCode()); + assertEquals(9L, ev.getOperatorUserId()); + assertTrue(ev.getUserIds().contains(5L)); + assertEquals("登录改造", ev.getParams().get("requirementTitle")); + assertEquals("灿能项目", ev.getParams().get("projectName")); + } + } + + /** + * Task 8:处理人为空时不发通知。 + */ + @Test + void createRequirement_whenHandlerNull_shouldNotPublish() { + ProjectRequirementDO req = new ProjectRequirementDO(); + req.setId(1L); + req.setProjectId(10L); + req.setTitle("登录改造"); + req.setCurrentHandlerUserId(null); + + projectRequirementService.publishRequirementAssignedNotify(req); + verify(applicationEventPublisher, never()).publishEvent(any()); + } + private ProjectRequirementDO buildRequirement(Long id, Long projectId, String statusCode) { ProjectRequirementDO requirement = new ProjectRequirementDO(); requirement.setId(id); diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectServiceImplTest.java index 81d2973..c89252d 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/ProjectServiceImplTest.java @@ -15,6 +15,7 @@ import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectP import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectRespVO; import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectSaveReqVO; import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectStatusActionReqVO; +import com.njcn.rdms.module.project.constant.ProjectObjectConstants; import com.njcn.rdms.module.project.service.datascope.ObjectDataScope; import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService; import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO; @@ -60,6 +61,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; @@ -88,6 +90,8 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest { @Mock private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService; @Mock + private com.njcn.rdms.module.project.service.member.CurrentUserRoleResolver currentUserRoleResolver; + @Mock private com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper projectRequirementModuleMapper; @Mock private ProjectStatusViewService projectStatusViewService; @@ -107,6 +111,10 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest { private com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper projectExecutionMapper; @Mock private com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper projectRequirementMapper; + @Mock + private com.njcn.rdms.module.project.framework.notify.TeamNotifyRecipientResolver teamNotifyRecipientResolver; + @Mock + private org.springframework.context.ApplicationEventPublisher applicationEventPublisher; @Test void getProjectDetail_shouldFillProductNameAndManagerNickname() { @@ -782,6 +790,33 @@ class ProjectServiceImplTest extends BaseMockitoUnitTest { .updateStatusByIdAndStatus(eq(projectId), eq("active"), eq("completed"), any()); } + @Test + void publishProjectCreatedNotify_perMemberWithRole() { + try (MockedStatic mocked = mockStatic(SecurityFrameworkUtils.class)) { + mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(9L); + ProjectDO project = new ProjectDO(); + project.setId(10L); + project.setProjectName("灿能项目"); + when(teamNotifyRecipientResolver.resolveActiveExcludingCreator( + eq(ProjectObjectConstants.OBJECT_TYPE), eq(10L), anyString(), anyString())) + .thenReturn(List.of( + new com.njcn.rdms.module.project.framework.notify.TeamRecipient(5L, "项目经理"), + new com.njcn.rdms.module.project.framework.notify.TeamRecipient(6L, "项目观察者"))); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.njcn.rdms.module.project.framework.notify.NotifySendEvent.class); + projectService.publishProjectCreatedNotify(project); + verify(applicationEventPublisher, times(2)).publishEvent(captor.capture()); + // 逐成员一条事件、各带自己的角色名 + com.njcn.rdms.module.project.framework.notify.NotifySendEvent first = captor.getAllValues().get(0); + assertEquals("project_created", first.getTemplateCode()); + assertEquals(9L, first.getOperatorUserId()); + assertEquals("项目经理", first.getParams().get("roleName")); + assertEquals("灿能项目", first.getParams().get("projectName")); + assertTrue(first.getUserIds().contains(5L)); + } + } + private ProjectSaveReqVO createReqVO(String code, Long productId, String projectName, Long managerUserId) { ProjectSaveReqVO reqVO = new ProjectSaveReqVO(); reqVO.setProjectCode(code); diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImplTest.java index 195c2fe..0d8d77f 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/execution/ProjectExecutionServiceImplTest.java @@ -33,6 +33,7 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; import com.njcn.rdms.module.project.enums.ErrorCodeConstants; import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants; +import com.njcn.rdms.module.project.framework.notify.NotifySendEvent; import com.njcn.rdms.module.project.service.project.ProjectRequirementService; import com.njcn.rdms.module.system.api.dict.DictDataApi; import com.njcn.rdms.module.system.api.user.AdminUserApi; @@ -57,6 +58,7 @@ 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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyCollection; @@ -105,6 +107,8 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest { private ProjectRequirementMapper projectRequirementMapper; @Mock private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver; + @Mock + private org.springframework.context.ApplicationEventPublisher applicationEventPublisher; /** * 默认让 dictDataApi.validateDictDataList 对 REQ_PRIORITY 返回 true,既有测试不因 priority 校验失败。 @@ -888,6 +892,38 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest { assertEquals(true, auditCaptor.getValue().getFieldChanges().contains("priority")); } + /** + * 创建执行后应 publish「执行指派」站内信事件:接收人 = 负责人 + 活跃协办人, + * 模板码 execution_assigned,operatorUserId = 当前登录用户(监听器统一排除),params 含项目名 / 执行名。 + */ + @Test + void createExecution_publishesAssignedNotify() { + try (MockedStatic mocked = mockStatic(SecurityFrameworkUtils.class)) { + mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(7L); + ProjectExecutionDO exec = new ProjectExecutionDO(); + exec.setId(1L); + exec.setProjectId(10L); + exec.setExecutionName("一阶段"); + exec.setOwnerId(5L); + ExecutionAssigneeDO a = new ExecutionAssigneeDO(); + a.setUserId(6L); + when(executionAssigneeMapper.selectActiveListByExecutionId(1L)).thenReturn(List.of(a)); + ProjectDO project = new ProjectDO(); + project.setProjectName("灿能项目"); + when(projectMapper.selectById(10L)).thenReturn(project); + + ArgumentCaptor captor = ArgumentCaptor.forClass(NotifySendEvent.class); + projectExecutionService.publishExecutionAssignedNotify(exec); + verify(applicationEventPublisher).publishEvent(captor.capture()); + NotifySendEvent ev = captor.getValue(); + assertEquals("execution_assigned", ev.getTemplateCode()); + assertEquals(7L, ev.getOperatorUserId()); + assertTrue(ev.getUserIds().containsAll(List.of(5L, 6L))); + assertEquals("灿能项目", ev.getParams().get("projectName")); + assertEquals("一阶段", ev.getParams().get("executionName")); + } + } + private ProjectDO createEditableProject(Long projectId) { ProjectDO project = new ProjectDO(); project.setId(projectId); diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImplTest.java index 4d8ad97..4fe1d28 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/project/task/ProjectTaskServiceImplTest.java @@ -1,5 +1,6 @@ package com.njcn.rdms.module.project.service.project.task; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest; import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO; @@ -12,12 +13,16 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.springframework.context.ApplicationEventPublisher; import java.util.Arrays; +import java.util.List; 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.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -65,4 +70,24 @@ class ProjectTaskServiceImplTest extends BaseMockitoUnitTest { assertEquals("演示项目", event.getParams().get("projectName")); assertEquals("联调任务", event.getParams().get("taskName")); } + + /** + * 事件须携带操作人(登录用户)id,供监听器统一从收件人里排除操作人自己。 + */ + @Test + void publishTaskAssignedNotify_carriesOperator() { + try (MockedStatic mocked = mockStatic(SecurityFrameworkUtils.class)) { + mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(99L); + ProjectTaskDO task = new ProjectTaskDO(); + task.setId(1L); + task.setTaskTitle("接口联调"); + when(taskAssigneeMapper.selectActiveListByTaskId(1L)).thenReturn(List.of()); + when(projectMapper.selectById(any())).thenReturn(null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(NotifySendEvent.class); + projectTaskService.publishTaskAssignedNotify(10L, task, 5L); + verify(applicationEventPublisher).publishEvent(captor.capture()); + assertEquals(99L, captor.getValue().getOperatorUserId()); + } + } } diff --git a/rdms-system/rdms-system-boot/pom.xml b/rdms-system/rdms-system-boot/pom.xml index 4511ccb..3e95632 100644 --- a/rdms-system/rdms-system-boot/pom.xml +++ b/rdms-system/rdms-system-boot/pom.xml @@ -140,7 +140,9 @@ spring-boot-maven-plugin ${spring.boot.version} - true + + false diff --git a/rdms-system/rdms-system-boot/src/main/resources/application-dev.yaml b/rdms-system/rdms-system-boot/src/main/resources/application-dev.yaml deleted file mode 100644 index 6322d31..0000000 --- a/rdms-system/rdms-system-boot/src/main/resources/application-dev.yaml +++ /dev/null @@ -1,92 +0,0 @@ -#################### 注册中心 + 配置中心相关配置 #################### - -spring: - cloud: - nacos: - server-addr: 192.168.1.103:18848 # Nacos 服务器地址 - username: # Nacos 账号 - password: # Nacos 密码 - discovery: # 【配置中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - metadata: - version: 1.0.0 # 服务实例的版本号,可用于灰度发布 - config: # 【注册中心】配置项 - namespace: 1924bcfb-4eab-4c58-9003-4a37d5fc2949 # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - -#################### 数据库相关配置 #################### - # 数据源配置项 - autoconfigure: - exclude: - datasource: - druid: # Druid 【监控】相关的全局配置 - web-stat-filter: - enabled: true - stat-view-servlet: - enabled: true - allow: # 设置白名单,不填则允许所有访问 - url-pattern: /druid/* - login-username: # 控制台管理用户名和密码 - login-password: - filter: - stat: - enabled: true - log-slow-sql: true # 慢 SQL 记录 - slow-sql-millis: 100 - merge-sql: true - wall: - config: - multi-statement-allow: true - dynamic: # 多数据源配置 - druid: # Druid 【连接池】相关的全局配置 - initial-size: 5 # 初始连接数 - min-idle: 10 # 最小连接池数量 - max-active: 20 # 最大连接池数量 - max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) - min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) - max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) - validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 - test-while-idle: true - test-on-borrow: false - test-on-return: false - pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 - max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 - primary: master - datasource: - master: - url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 - username: root - password: njcnpqs - # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 - data: - redis: - host: 192.168.1.22 # 地址 - port: 16379 # 端口 - database: 1 # 数据库索引 -# password: njcnpqs # 密码,建议生产环境开启 - - -#################### 监控相关配置 #################### - -# Actuator 监控端点的配置项 -management: - endpoints: - web: - base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator - exposure: - include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 - - - -# 日志文件配置 -logging: - file: - name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 - -#################### 灿能相关配置 #################### - -# 灿能配置项,设置当前项目所有自定义的配置 -rdms: - demo: true # 开启演示模式 diff --git a/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml b/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml deleted file mode 100644 index 698521b..0000000 --- a/rdms-system/rdms-system-boot/src/main/resources/application-local.yaml +++ /dev/null @@ -1,100 +0,0 @@ -#################### 注册中心 + 配置中心相关配置 #################### -spring: - cloud: - nacos: - server-addr: 192.168.1.103:18848 # Nacos 服务器地址 - username: # Nacos 账号 - password: # Nacos 密码 - discovery: # 【配置中心】配置项 - namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - metadata: - version: 1.0.0 # 服务实例的版本号,可用于灰度发布 - config: # 【注册中心】配置项 - namespace: 0cd9c1b2-56ba-4e1d-a23b-f951392c46bf # 命名空间。这里使用 dev 开发环境 - group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP - -#################### 数据库相关配置 #################### - # 数据源配置项 - autoconfigure: - exclude: - datasource: - druid: # Druid 【监控】相关的全局配置 - web-stat-filter: - enabled: true - stat-view-servlet: - enabled: true - allow: # 设置白名单,不填则允许所有访问 - url-pattern: /druid/* - login-username: # 控制台管理用户名和密码 - login-password: - filter: - stat: - enabled: true - log-slow-sql: true # 慢 SQL 记录 - slow-sql-millis: 100 - merge-sql: true - wall: - config: - multi-statement-allow: true - dynamic: # 多数据源配置 - druid: # Druid 【连接池】相关的全局配置 - initial-size: 5 # 初始连接数 - min-idle: 10 # 最小连接池数量 - max-active: 20 # 最大连接池数量 - max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) - min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) - max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) - validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 - test-while-idle: true - test-on-borrow: false - test-on-return: false - pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 - max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 - primary: master - datasource: - master: - url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 - username: root - password: njcnpqs - - # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 - data: - redis: - host: 127.0.0.1 # 地址 - port: 6379 # 端口 - database: 1 # 数据库索引 -# password: njcnpqs # 密码,建议生产环境开启 - - -#################### 监控相关配置 #################### - -# Actuator 监控端点的配置项 -management: - endpoints: - web: - base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator - exposure: - include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 - - -# 日志文件配置 -logging: - level: - # 配置自己写的 MyBatis Mapper 打印日志 - com.njcn.rdms.module.system.dal.mysql: debug - com.njcn.rdms.module.system.dal.mysql.logger.ApiErrorLogMapper: INFO # 配置 ApiErrorLogMapper 的日志级别为 info,避免和 GlobalExceptionHandler 重复打印 - com.njcn.rdms.module.system.dal.mysql.file.FileConfigMapper: INFO # 配置 FileConfigMapper 的日志级别为 info - org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR - -# 灿能配置项,设置当前项目所有自定义的配置 -rdms: - env: # 多环境的配置项 - tag: ${HOSTNAME} - captcha: - enable: false - security: - mock-enable: true - access-log: # 访问日志的配置项 - enable: true diff --git a/rdms-system/rdms-system-boot/src/main/resources/application.yaml b/rdms-system/rdms-system-boot/src/main/resources/application.yaml index 8a4d86d..9c6fc14 100644 --- a/rdms-system/rdms-system-boot/src/main/resources/application.yaml +++ b/rdms-system/rdms-system-boot/src/main/resources/application.yaml @@ -1,15 +1,28 @@ spring: application: name: rdms-system-server - profiles: - active: local main: allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务 + cloud: + # 注册中心 + 配置中心连接(值由根 pom 的 nacos.* 属性在打包时注入) + nacos: + server-addr: @nacos.server-addr@ + username: @nacos.username@ + password: @nacos.password@ + discovery: + namespace: @nacos.namespace@ + group: @nacos.group@ + metadata: + version: ${rdms.info.version} # 灰度发布用的实例版本号 + config: + namespace: @nacos.namespace@ + group: @nacos.group@ config: import: - - optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置 - - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 + - nacos:rdms-common.yaml # 公共非敏感配置(数据库地址/Redis/开关等) + - nacos:rdms-common-secret.yaml # 公共敏感配置(数据库账密/加解密秘钥) + - nacos:rdms-system-server-secret.yaml # system 独有敏感配置(RSA 私钥) # Servlet 配置 servlet: # 文件上传相关配置项 @@ -47,6 +60,10 @@ server: logging: file: name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + level: + com.njcn.rdms.module.system.dal.mysql: debug # 打印本模块 Mapper 的 SQL 日志 + com.njcn.rdms.module.system.dal.mysql.logger.ApiErrorLogMapper: INFO # 避免和 GlobalExceptionHandler 重复打印 + com.njcn.rdms.module.system.dal.mysql.file.FileConfigMapper: INFO --- #################### 接口文档配置 #################### springdoc: @@ -76,8 +93,7 @@ mybatis-plus: logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) banner: false # 关闭控制台的 Banner 打印 type-aliases-package: ${rdms.info.base-package}.dal.dataobject - encryptor: - password: cDHvwsYb9eyLNBHp # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成。数据库存密文,业务代码透明拿到明文(@EncryptField 注解字段自动加解密)。秘钥一旦变更,历史密文将无法解密,生产环境务必通过 Nacos 注入,切勿硬编码。 + # encryptor.password(@EncryptField 字段加解密秘钥)已外置到 Nacos rdms-common-secret.yaml,不再硬编码进 git mybatis-plus-join: banner: false # 关闭控制台的 Banner 打印 @@ -131,6 +147,6 @@ rdms: enable: true # 启用密码相关接口的请求解密能力 header: X-Api-Encrypt # 请求加密标记头 algorithm: RSA # 密码相关接口的请求体采用 RSA 非对称加密 - request-key: 'MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC/aShtWjlpINa+ZZkgp4sbt2jA4tPCN1YjDLv5SZMHDd7q8lbkE0SOudbuSKp5P3tVCPZXowyZom5+l56AAIYCaG5OcbzeRUtB6JcvmuU9SZ008zw7z2BIzeIzMtJSGf6u8BocVeMo27bGyyh1ifUXbpKVU7V7DBLzYADAQ9Jqi0vsqrxDGDu+Zm3LpFwSOnv85pgC0d+9re57CIYynXVmTLAo+V5DedPsceNCAByRs1kUyFMwyoPNbmgjcpKbewD6laxR9GtnFR/bCzfnz8Up7ANtuHCPe7vfU1teU75ZR+/cW9t2GS1e1T/XkULRv5PH5gchSGQ1NHO4imIbv5dzAgMBAAECggEACTjSS051BKUh44N2mLWpxJiWEfD7vdg3rLGg3tZWIJlg+5XYbN2myG+YtNtIZ1YRJZwsbjV7Vm2WgD/i0Yz05+nLIrllHZpeEVtY6WC/ma/RxKrRZJpNq8RLmSbiLjV1aU1FHMdgjefkCvjfxqXyaoIXyt0BGeAPi6087AZ4fUyKVYgPyGr53RnD8+4nCDaRhZYMCv6zpb+YVF3llZZNhvK7+hDLZX0WhUgIAzStzFsPZhDfJxW8MQFB4FNtmnJ4kpInkgIAROlfVvKIwRKwoCH+sveGjYdlZR/wTYt6HQoKudG9Qx2IssUcVGFwAsCiWM+81rfBDd5pMUwzyGQ9OQKBgQDHOp7Eio4M6LaPO1Uz6Ozlp28evWBVPaU+wk50p5SQl//pF0VgDkmrrt3Wu9IppBL6VObIzjOsZJrEVHXheA/1qqOVYm/m6nel1EUAqbIqxREtw+GJPoKp3Ql1CxK6pvm/KxOhJvCDIUNCZ4in+rvsCvquF784iIbQ33ED3hWi2wKBgQD19DbAL1Y6/XHXX17t6yZJVsIijmSOo5tjeNHouOSP5emgc8i2ESaW4WPIzkgi7EJ2aertgUkwIOpunYvMWYfn6zrYNaSuvCCZF+6oIiYPPXEVZJTnzGA/KsJtHeH6xtiGuettw6RnPxXvNZibJhfLdOqQvZmRDRTXh/MiRuelSQKBgQC154IbNd7pTnmRYb0zvlK+hRfiW0rfyX9dRBBaVsBBHWedrY+8Wo9NYEZQ0ADd4F8rjeWCJzPrDZh59hwDl5oK1pixxsUhc6d3E89FAawZfQFoZddBdn/bFGSUJ14camTR9UTg+SrUr8Q3l0yhA0AeDxA/cJM5zP47LCiGPXpHzQKBgQDV00sGKiE9h7nBFBjjntvaRqLgiArEN1iQUimruZJ7x9YkuIR2RNLXuXuWyD/OnLfrWonzkcKfJP6qzC0Nq4iMB+VQstJJVyS/9B537bhI55G4l4kdPIEwaWw+kQw1iUoVVu1mr//uAtp+7ImP2L43E54Z17v6bvT/rCGkWyBogQKBgQC6pqnciYteAE5KmWnPM9LWoEorSBPCzbWCVwuja7NbVoADUPvAnUeDgvKs8KpWvL+X3eRGSZXOBqjBMsdDPBnQzr5yZCI3Mv6Svg9RxBfuWw1mF1w2GAwK1r7+6ZDwxFqRUiVUACRRJ8S1kBa+CvNWm7UFi/7V1D4UDyKKmBU6Sw==' + # request-key(RSA 私钥)已外置到 Nacos rdms-system-server-secret.yaml,不再硬编码进 git debug: false