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