From ae90dcec24ba74256958efe5740d3a0df99589ac Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Fri, 24 Apr 2026 16:22:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(project):=20=E4=B8=BA=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E6=97=B6=E9=97=B4=E7=BA=BF=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=88=90=E5=91=98=E8=A7=92=E8=89=B2=E5=90=8D=E7=A7=B0=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ObjectActivityConstants 中添加 MEMBER_ACTION_UPDATE 类型支持 - 为 ProductActivityQueryService 和 ProductActivityTimelineQueryService 添加角色名称加载和缓存功能 - 实现角色名称解析和 JSON 数据结构扩展 - 添加相关单元测试验证角色名称显示逻辑 - 集成 ObjectPermissionApi 获取角色信息并实现缓存机制 --- .../constant/ObjectActivityConstants.java | 2 +- .../product/ProductActivityQueryService.java | 143 +++++++++++++++++- .../ProductActivityTimelineQueryService.java | 108 +++++++++++++ .../ProductActivityQueryServiceTest.java | 100 ++++++++++++ ...oductActivityTimelineQueryServiceTest.java | 74 +++++++++ 5 files changed, 425 insertions(+), 2 deletions(-) diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ObjectActivityConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ObjectActivityConstants.java index aee265a..930e09e 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ObjectActivityConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/ObjectActivityConstants.java @@ -50,7 +50,7 @@ public final class ObjectActivityConstants { PRODUCT_ACTION_CREATE, PRODUCT_ACTION_CHANGE_MANAGER); public static final List MEMBER_TIMELINE_ACTION_TYPES = List.of( - MEMBER_ACTION_ADD, MEMBER_ACTION_REMOVE); + MEMBER_ACTION_ADD, MEMBER_ACTION_UPDATE, MEMBER_ACTION_REMOVE); private static final Set STATUS_ACTION_TYPE_SET = Set.copyOf(STATUS_ACTION_TYPES); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductActivityQueryService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductActivityQueryService.java index 748468d..2e42f91 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductActivityQueryService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductActivityQueryService.java @@ -1,5 +1,7 @@ package com.njcn.rdms.module.project.service.product; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.common.util.json.JsonUtils; import com.njcn.rdms.framework.common.util.object.PageUtils; @@ -12,7 +14,11 @@ import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO; 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.mysql.product.ProductStatusLogMapper; +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.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -20,9 +26,11 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -31,6 +39,8 @@ public class ProductActivityQueryService { private static final String PRODUCT_OBJECT_TYPE = ObjectActivityConstants.PRODUCT_BIZ_TYPE; private static final String MEMBER_BIZ_TYPE = ObjectActivityConstants.MEMBER_BIZ_TYPE; + private static final String ACTIVITY_ROLE_NAME_CACHE = "project_activity_role_name#10m"; + private static final String ROLE_SCOPE_OBJECT = "object"; @Resource private ProductStatusLogMapper productStatusLogMapper; @@ -38,6 +48,10 @@ public class ProductActivityQueryService { private BizAuditLogMapper bizAuditLogMapper; @Resource private UserObjectRoleMapper userObjectRoleMapper; + @Resource + private CacheManager cacheManager; + @Resource + private ObjectPermissionApi objectPermissionApi; public PageResult getProductActivities(Long productId, ProductActivityPageReqVO reqVO) { List items = new ArrayList<>(); @@ -60,7 +74,9 @@ public class ProductActivityQueryService { List activities = items.stream() .map(ActivityItem::respVO) .toList(); - return buildPageResult(activities, reqVO); + PageResult pageResult = buildPageResult(activities, reqVO); + fillMemberRoleNames(pageResult.getList()); + return pageResult; } private void appendMemberActivities(Long productId, ProductActivityPageReqVO reqVO, List items) { @@ -101,6 +117,131 @@ public class ProductActivityQueryService { return new PageResult<>(activities.subList(start, end), (long) activities.size()); } + private void fillMemberRoleNames(List activities) { + if (activities == null || activities.isEmpty()) { + return; + } + Set roleIds = new LinkedHashSet<>(); + for (ProductActivityRespVO activity : activities) { + if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) { + continue; + } + Long beforeRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "before"); + Long afterRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "after"); + if (beforeRoleId != null) { + roleIds.add(beforeRoleId); + } + if (afterRoleId != null) { + roleIds.add(afterRoleId); + } + } + if (roleIds.isEmpty()) { + return; + } + Map roleNameMap = loadRoleNameMap(roleIds); + for (ProductActivityRespVO activity : activities) { + if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) { + continue; + } + activity.setDetails(appendRoleNames(activity.getDetails(), roleNameMap)); + } + } + + private Map loadRoleNameMap(Set roleIds) { + Map roleNameMap = new LinkedHashMap<>(); + if (roleIds == null || roleIds.isEmpty()) { + return roleNameMap; + } + Cache cache = cacheManager == null ? null : cacheManager.getCache(ACTIVITY_ROLE_NAME_CACHE); + Set missIds = new LinkedHashSet<>(); + for (Long roleId : roleIds) { + String roleName = cache == null ? null : cache.get(roleId, String.class); + if (roleName != null) { + roleNameMap.put(roleId, roleName); + } else { + missIds.add(roleId); + } + } + if (missIds.isEmpty()) { + return roleNameMap; + } + Map roleMap = objectPermissionApi == null ? Map.of() + : objectPermissionApi.getObjectRoleMap(missIds, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE); + if (roleMap == null || roleMap.isEmpty()) { + return roleNameMap; + } + for (Long roleId : missIds) { + ObjectRoleRespDTO role = roleMap.get(roleId); + if (role == null || !StringUtils.hasText(role.getName())) { + continue; + } + roleNameMap.put(roleId, role.getName()); + if (cache != null) { + cache.put(roleId, role.getName()); + } + } + return roleNameMap; + } + + private Long getFieldChangeLong(String fieldChanges, String fieldName, String valueField) { + JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField); + if (valueNode == null || valueNode.isNull()) { + return null; + } + if (valueNode.isNumber()) { + return valueNode.longValue(); + } + if (valueNode.isTextual() && StringUtils.hasText(valueNode.textValue())) { + return Long.valueOf(valueNode.textValue().trim()); + } + return null; + } + + private JsonNode getFieldChangeNode(String fieldChanges, String fieldName, String valueField) { + if (!StringUtils.hasText(fieldChanges) || !JsonUtils.isJsonObject(fieldChanges)) { + return null; + } + JsonNode fieldNode = JsonUtils.parseTree(fieldChanges).path(fieldName); + if (fieldNode.isMissingNode()) { + return null; + } + JsonNode valueNode = fieldNode.path(valueField); + return valueNode.isMissingNode() ? null : valueNode; + } + + private String appendRoleNames(String details, Map roleNameMap) { + if (!StringUtils.hasText(details) || !JsonUtils.isJsonObject(details)) { + return details; + } + Long beforeRoleId = getFieldChangeLong(details, "roleId", "before"); + Long afterRoleId = getFieldChangeLong(details, "roleId", "after"); + if (beforeRoleId == null && afterRoleId == null) { + return details; + } + JsonNode detailsNode = JsonUtils.parseTree(details); + if (!(detailsNode instanceof ObjectNode)) { + return details; + } + ObjectNode objectNode = ((ObjectNode) detailsNode).deepCopy(); + ObjectNode roleNameNode = objectNode.putObject("roleName"); + appendRoleName(roleNameNode, "before", beforeRoleId, roleNameMap); + appendRoleName(roleNameNode, "after", afterRoleId, roleNameMap); + return JsonUtils.toJsonString(objectNode); + } + + private void appendRoleName(ObjectNode roleNameNode, String fieldName, Long roleId, Map roleNameMap) { + if (roleId == null) { + roleNameNode.putNull(fieldName); + return; + } + String roleName = roleNameMap.get(roleId); + if (StringUtils.hasText(roleName)) { + roleNameNode.put(fieldName, roleName); + return; + } + roleNameNode.putNull(fieldName); + } + private boolean includeType(String actual, String expected) { return !StringUtils.hasText(actual) || Objects.equals(actual.trim(), expected); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductActivityTimelineQueryService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductActivityTimelineQueryService.java index 4bf3a82..2119171 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductActivityTimelineQueryService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/product/ProductActivityTimelineQueryService.java @@ -1,6 +1,7 @@ package com.njcn.rdms.module.project.service.product; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.framework.common.util.json.JsonUtils; import com.njcn.rdms.framework.common.util.object.PageUtils; @@ -13,6 +14,8 @@ import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO; 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.mysql.product.ProductStatusLogMapper; +import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO; import com.njcn.rdms.module.system.api.user.AdminUserApi; import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; @@ -40,6 +43,9 @@ public class ProductActivityTimelineQueryService { * 成员名称在读取时间线时再通过缓存转换,避免把昵称快照写进动态记录。 */ private static final String TIMELINE_USER_NICKNAME_CACHE = "project_timeline_user_nickname#10m"; + private static final String TIMELINE_ROLE_NAME_CACHE = "project_timeline_role_name#10m"; + private static final String ROLE_SCOPE_OBJECT = "object"; + private static final String PRODUCT_OBJECT_TYPE = ObjectActivityConstants.PRODUCT_BIZ_TYPE; @Resource private ProductStatusLogMapper productStatusLogMapper; @@ -51,6 +57,8 @@ public class ProductActivityTimelineQueryService { private CacheManager cacheManager; @Resource private AdminUserApi adminUserApi; + @Resource + private ObjectPermissionApi objectPermissionApi; public PageResult getProductActivityTimelinePage( Long productId, ProductActivityTimelinePageReqVO reqVO) { @@ -68,6 +76,7 @@ public class ProductActivityTimelineQueryService { PageResult pageResult = buildPageResult(items.stream().map(ActivityItem::respVO).toList(), reqVO); fillTargetUserNames(pageResult.getList()); + fillMemberRoleNames(pageResult.getList()); return pageResult; } @@ -309,6 +318,36 @@ public class ProductActivityTimelineQueryService { } } + private void fillMemberRoleNames(List activities) { + if (activities == null || activities.isEmpty()) { + return; + } + Set roleIds = new LinkedHashSet<>(); + for (ProductActivityTimelineRespVO activity : activities) { + if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) { + continue; + } + Long beforeRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "before"); + Long afterRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "after"); + if (beforeRoleId != null) { + roleIds.add(beforeRoleId); + } + if (afterRoleId != null) { + roleIds.add(afterRoleId); + } + } + if (roleIds.isEmpty()) { + return; + } + Map roleNameMap = loadRoleNameMap(roleIds); + for (ProductActivityTimelineRespVO activity : activities) { + if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) { + continue; + } + activity.setDetails(appendRoleNames(activity.getDetails(), roleNameMap)); + } + } + private Map loadUserNicknameMap(Set userIds) { Map nicknameMap = new LinkedHashMap<>(); if (userIds == null || userIds.isEmpty()) { @@ -344,6 +383,75 @@ public class ProductActivityTimelineQueryService { return nicknameMap; } + private Map loadRoleNameMap(Set roleIds) { + Map roleNameMap = new LinkedHashMap<>(); + if (roleIds == null || roleIds.isEmpty()) { + return roleNameMap; + } + Cache cache = cacheManager == null ? null : cacheManager.getCache(TIMELINE_ROLE_NAME_CACHE); + Set missIds = new LinkedHashSet<>(); + for (Long roleId : roleIds) { + String roleName = cache == null ? null : cache.get(roleId, String.class); + if (roleName != null) { + roleNameMap.put(roleId, roleName); + } else { + missIds.add(roleId); + } + } + if (missIds.isEmpty()) { + return roleNameMap; + } + Map roleMap = objectPermissionApi == null ? Map.of() + : objectPermissionApi.getObjectRoleMap(missIds, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE); + if (roleMap == null || roleMap.isEmpty()) { + return roleNameMap; + } + for (Long roleId : missIds) { + ObjectRoleRespDTO role = roleMap.get(roleId); + if (role == null || !StringUtils.hasText(role.getName())) { + continue; + } + roleNameMap.put(roleId, role.getName()); + if (cache != null) { + cache.put(roleId, role.getName()); + } + } + return roleNameMap; + } + + private String appendRoleNames(String details, Map roleNameMap) { + if (!StringUtils.hasText(details) || !JsonUtils.isJsonObject(details)) { + return details; + } + Long beforeRoleId = getFieldChangeLong(details, "roleId", "before"); + Long afterRoleId = getFieldChangeLong(details, "roleId", "after"); + if (beforeRoleId == null && afterRoleId == null) { + return details; + } + JsonNode detailsNode = JsonUtils.parseTree(details); + if (!(detailsNode instanceof ObjectNode)) { + return details; + } + ObjectNode objectNode = ((ObjectNode) detailsNode).deepCopy(); + ObjectNode roleNameNode = objectNode.putObject("roleName"); + appendRoleName(roleNameNode, "before", beforeRoleId, roleNameMap); + appendRoleName(roleNameNode, "after", afterRoleId, roleNameMap); + return JsonUtils.toJsonString(objectNode); + } + + private void appendRoleName(ObjectNode roleNameNode, String fieldName, Long roleId, Map roleNameMap) { + if (roleId == null) { + roleNameNode.putNull(fieldName); + return; + } + String roleName = roleNameMap.get(roleId); + if (StringUtils.hasText(roleName)) { + roleNameNode.put(fieldName, roleName); + return; + } + roleNameNode.putNull(fieldName); + } + private ProductActivityTimelineRespVO toStatusTimeline(ProductStatusLogDO log) { ProductActivityTimelineRespVO respVO = new ProductActivityTimelineRespVO(); respVO.setId(ObjectActivityConstants.ACTIVITY_TYPE_STATUS + ":" + log.getId()); diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductActivityQueryServiceTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductActivityQueryServiceTest.java index 5c5f2e3..3b189cc 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductActivityQueryServiceTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductActivityQueryServiceTest.java @@ -1,6 +1,7 @@ package com.njcn.rdms.module.project.service.product; import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.common.util.json.JsonUtils; import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest; import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityPageReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityRespVO; @@ -10,14 +11,23 @@ import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO; 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.mysql.product.ProductStatusLogMapper; +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.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.mockito.InjectMocks; import org.mockito.Mock; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ProductActivityQueryServiceTest extends BaseMockitoUnitTest { @@ -30,6 +40,12 @@ class ProductActivityQueryServiceTest extends BaseMockitoUnitTest { private BizAuditLogMapper bizAuditLogMapper; @Mock private UserObjectRoleMapper userObjectRoleMapper; + @Mock + private CacheManager cacheManager; + @Mock + private Cache roleNameCache; + @Mock + private ObjectPermissionApi objectPermissionApi; @Test void getProductActivities_shouldMergeStatusProductAndMemberActivities() { @@ -110,4 +126,88 @@ class ProductActivityQueryServiceTest extends BaseMockitoUnitTest { assertEquals(0, result.getList().size()); } + @Test + void getProductActivities_shouldAppendRoleNameToMemberDetails() { + Long productId = 1003L; + ProductActivityPageReqVO reqVO = new ProductActivityPageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + reqVO.setActivityType("member"); + + BizAuditLogDO memberAudit = new BizAuditLogDO(); + memberAudit.setId(55L); + memberAudit.setBizType("rdms_user_object_role"); + memberAudit.setBizId(9002L); + memberAudit.setActionType("update_member"); + memberAudit.setFieldChanges("{\"roleId\":{\"before\":3201,\"after\":3202}}"); + memberAudit.setCreateTime(LocalDateTime.of(2026, 4, 21, 13, 0, 0)); + + UserObjectRoleDO member = new UserObjectRoleDO(); + member.setId(9002L); + member.setObjectType("product"); + member.setObjectId(productId); + + when(bizAuditLogMapper.selectListByBizType("rdms_user_object_role", null, null)) + .thenReturn(List.of(memberAudit)); + when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9002L), "product", productId)) + .thenReturn(List.of(member)); + when(cacheManager.getCache("project_activity_role_name#10m")).thenReturn(roleNameCache); + when(roleNameCache.get(3201L, String.class)).thenReturn("产品经理"); + when(roleNameCache.get(3202L, String.class)).thenReturn("产品成员"); + + PageResult result = productActivityQueryService.getProductActivities(productId, reqVO); + + assertEquals("产品经理", JsonUtils.parseTree(result.getList().get(0).getDetails()) + .path("roleName").path("before").asText()); + assertEquals("产品成员", JsonUtils.parseTree(result.getList().get(0).getDetails()) + .path("roleName").path("after").asText()); + verify(objectPermissionApi, never()).getObjectRoleMap(anySet(), any(), any()); + } + + @Test + void getProductActivities_shouldLoadAndCacheRoleNameWhenCacheMiss() { + Long productId = 1004L; + ProductActivityPageReqVO reqVO = new ProductActivityPageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + reqVO.setActivityType("member"); + + BizAuditLogDO memberAudit = new BizAuditLogDO(); + memberAudit.setId(66L); + memberAudit.setBizType("rdms_user_object_role"); + memberAudit.setBizId(9003L); + memberAudit.setActionType("add_member"); + memberAudit.setFieldChanges("{\"roleId\":{\"before\":null,\"after\":3203}}"); + memberAudit.setCreateTime(LocalDateTime.of(2026, 4, 21, 14, 0, 0)); + + UserObjectRoleDO member = new UserObjectRoleDO(); + member.setId(9003L); + member.setObjectType("product"); + member.setObjectId(productId); + + when(bizAuditLogMapper.selectListByBizType("rdms_user_object_role", null, null)) + .thenReturn(List.of(memberAudit)); + when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9003L), "product", productId)) + .thenReturn(List.of(member)); + when(cacheManager.getCache("project_activity_role_name#10m")).thenReturn(roleNameCache); + when(roleNameCache.get(3203L, String.class)).thenReturn(null); + when(objectPermissionApi.getObjectRoleMap(java.util.Set.of(3203L), "object", "product")) + .thenReturn(Map.of(3203L, buildRole(3203L, "观察者"))); + + PageResult result = productActivityQueryService.getProductActivities(productId, reqVO); + + assertEquals("观察者", JsonUtils.parseTree(result.getList().get(0).getDetails()) + .path("roleName").path("after").asText()); + verify(roleNameCache).put(3203L, "观察者"); + } + + private ObjectRoleRespDTO buildRole(Long id, String name) { + ObjectRoleRespDTO role = new ObjectRoleRespDTO(); + role.setId(id); + role.setName(name); + role.setScopeType("object"); + role.setObjectType("product"); + return role; + } + } diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductActivityTimelineQueryServiceTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductActivityTimelineQueryServiceTest.java index fb969a2..acac506 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductActivityTimelineQueryServiceTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductActivityTimelineQueryServiceTest.java @@ -2,6 +2,7 @@ package com.njcn.rdms.module.project.service.product; import com.njcn.rdms.framework.common.exception.ServiceException; import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.common.util.json.JsonUtils; import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest; import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelinePageReqVO; import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelineRespVO; @@ -11,6 +12,8 @@ import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO; 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.mysql.product.ProductStatusLogMapper; +import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; +import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO; import com.njcn.rdms.module.system.api.user.AdminUserApi; import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; import org.junit.jupiter.api.Test; @@ -51,7 +54,11 @@ class ProductActivityTimelineQueryServiceTest extends BaseMockitoUnitTest { @Mock private Cache userNicknameCache; @Mock + private Cache roleNameCache; + @Mock private AdminUserApi adminUserApi; + @Mock + private ObjectPermissionApi objectPermissionApi; @Test void getProductActivityTimelinePage_whenTimeRangeMissingEnd_shouldThrowInvalidParam() { @@ -289,6 +296,64 @@ class ProductActivityTimelineQueryServiceTest extends BaseMockitoUnitTest { verify(userNicknameCache).put(2005L, "成员戊"); } + @Test + void getProductActivityTimelinePage_shouldReadRoleNameFromCacheFirst() { + Long productId = 1009L; + ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + reqVO.setActivityType("member"); + + BizAuditLogDO addMemberLog = buildMemberLog(81L, 9006L, "add_member", + "{\"roleId\":{\"before\":null,\"after\":3101},\"userId\":{\"before\":null,\"after\":2006}}", + LocalDateTime.of(2026, 4, 21, 19, 0, 0)); + when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(), + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(List.of(addMemberLog)); + when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9006L), "product", productId)) + .thenReturn(List.of(buildMember(9006L, productId, 2006L))); + when(cacheManager.getCache("project_timeline_user_nickname#10m")).thenReturn(userNicknameCache); + when(cacheManager.getCache("project_timeline_role_name#10m")).thenReturn(roleNameCache); + when(roleNameCache.get(3101L, String.class)).thenReturn("产品经理"); + + PageResult result = + productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO); + + assertEquals("产品经理", JsonUtils.parseTree(result.getList().get(0).getDetails()) + .path("roleName").path("after").asText()); + verify(objectPermissionApi, never()).getObjectRoleMap(anySet(), any(), any()); + } + + @Test + void getProductActivityTimelinePage_shouldLoadAndCacheRoleNameWhenCacheMiss() { + Long productId = 1010L; + ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + reqVO.setActivityType("member"); + + BizAuditLogDO removeMemberLog = buildMemberLog(91L, 9007L, "remove_member", + "{\"roleId\":{\"before\":3102,\"after\":3102},\"userId\":{\"before\":2007,\"after\":2007}}", + LocalDateTime.of(2026, 4, 21, 20, 0, 0)); + when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(), + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(List.of(removeMemberLog)); + when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9007L), "product", productId)) + .thenReturn(List.of(buildMember(9007L, productId, 2007L))); + when(cacheManager.getCache("project_timeline_user_nickname#10m")).thenReturn(userNicknameCache); + when(cacheManager.getCache("project_timeline_role_name#10m")).thenReturn(roleNameCache); + when(roleNameCache.get(3102L, String.class)).thenReturn(null); + when(objectPermissionApi.getObjectRoleMap(java.util.Set.of(3102L), "object", "product")) + .thenReturn(Map.of(3102L, buildRole(3102L, "产品成员"))); + + PageResult result = + productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO); + + assertEquals("产品成员", JsonUtils.parseTree(result.getList().get(0).getDetails()) + .path("roleName").path("before").asText()); + verify(roleNameCache).put(3102L, "产品成员"); + } + private BizAuditLogDO buildProductLog(Long id, Long productId, String actionType, String fieldChanges, LocalDateTime createTime) { BizAuditLogDO log = new BizAuditLogDO(); @@ -347,4 +412,13 @@ class ProductActivityTimelineQueryServiceTest extends BaseMockitoUnitTest { return user; } + private ObjectRoleRespDTO buildRole(Long id, String name) { + ObjectRoleRespDTO role = new ObjectRoleRespDTO(); + role.setId(id); + role.setName(name); + role.setScopeType("object"); + role.setObjectType("product"); + return role; + } + }