feat(project): 为项目活动时间线添加成员角色名称显示功能

- 在 ObjectActivityConstants 中添加 MEMBER_ACTION_UPDATE 类型支持
- 为 ProductActivityQueryService 和 ProductActivityTimelineQueryService
  添加角色名称加载和缓存功能
- 实现角色名称解析和 JSON 数据结构扩展
- 添加相关单元测试验证角色名称显示逻辑
- 集成 ObjectPermissionApi 获取角色信息并实现缓存机制
This commit is contained in:
2026-04-24 16:22:23 +08:00
parent ee732b97bf
commit ae90dcec24
5 changed files with 425 additions and 2 deletions

View File

@@ -50,7 +50,7 @@ public final class ObjectActivityConstants {
PRODUCT_ACTION_CREATE, PRODUCT_ACTION_CHANGE_MANAGER);
public static final List<String> 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<String> STATUS_ACTION_TYPE_SET = Set.copyOf(STATUS_ACTION_TYPES);

View File

@@ -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<ProductActivityRespVO> getProductActivities(Long productId, ProductActivityPageReqVO reqVO) {
List<ActivityItem> items = new ArrayList<>();
@@ -60,7 +74,9 @@ public class ProductActivityQueryService {
List<ProductActivityRespVO> activities = items.stream()
.map(ActivityItem::respVO)
.toList();
return buildPageResult(activities, reqVO);
PageResult<ProductActivityRespVO> pageResult = buildPageResult(activities, reqVO);
fillMemberRoleNames(pageResult.getList());
return pageResult;
}
private void appendMemberActivities(Long productId, ProductActivityPageReqVO reqVO, List<ActivityItem> items) {
@@ -101,6 +117,131 @@ public class ProductActivityQueryService {
return new PageResult<>(activities.subList(start, end), (long) activities.size());
}
private void fillMemberRoleNames(List<ProductActivityRespVO> activities) {
if (activities == null || activities.isEmpty()) {
return;
}
Set<Long> 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<Long, String> 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<Long, String> loadRoleNameMap(Set<Long> roleIds) {
Map<Long, String> roleNameMap = new LinkedHashMap<>();
if (roleIds == null || roleIds.isEmpty()) {
return roleNameMap;
}
Cache cache = cacheManager == null ? null : cacheManager.getCache(ACTIVITY_ROLE_NAME_CACHE);
Set<Long> 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<Long, ObjectRoleRespDTO> 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<Long, String> 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<Long, String> 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);
}

View File

@@ -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<ProductActivityTimelineRespVO> getProductActivityTimelinePage(
Long productId, ProductActivityTimelinePageReqVO reqVO) {
@@ -68,6 +76,7 @@ public class ProductActivityTimelineQueryService {
PageResult<ProductActivityTimelineRespVO> 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<ProductActivityTimelineRespVO> activities) {
if (activities == null || activities.isEmpty()) {
return;
}
Set<Long> 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<Long, String> 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<Long, String> loadUserNicknameMap(Set<Long> userIds) {
Map<Long, String> nicknameMap = new LinkedHashMap<>();
if (userIds == null || userIds.isEmpty()) {
@@ -344,6 +383,75 @@ public class ProductActivityTimelineQueryService {
return nicknameMap;
}
private Map<Long, String> loadRoleNameMap(Set<Long> roleIds) {
Map<Long, String> roleNameMap = new LinkedHashMap<>();
if (roleIds == null || roleIds.isEmpty()) {
return roleNameMap;
}
Cache cache = cacheManager == null ? null : cacheManager.getCache(TIMELINE_ROLE_NAME_CACHE);
Set<Long> 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<Long, ObjectRoleRespDTO> 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<Long, String> 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<Long, String> 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());

View File

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

View File

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