diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/user/SysUserRelationUpdateDto.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/user/SysUserRelationUpdateDto.java new file mode 100644 index 000000000..c94e7b5fb --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/user/SysUserRelationUpdateDto.java @@ -0,0 +1,49 @@ +package com.bsd.pjcloud.admin.api.dto.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 修改用户关系 + */ +@Data +@Schema(description = "修改用户关系") +public class SysUserRelationUpdateDto { + + @NotNull(message = "关系ID不能为空") + @Schema(description = "关系ID") + private Long relationId; + + @NotNull(message = "关系主语用户不能为空") + @Schema(description = "关系主语用户ID") + private Long subjectUserId; + + @NotNull(message = "关系对象用户不能为空") + @Schema(description = "关系对象用户ID") + private Long objectUserId; + + @NotNull(message = "关系类型不能为空") + @Schema(description = "关系类型ID") + private Long relationTypeId; + + @Schema(description = "关系生效时间") + private LocalDateTime effectiveTime; + + @Schema(description = "关系失效时间") + private LocalDateTime expireTime; + + @Size(max = 500, message = "关系备注不能超过500个字符") + @Schema(description = "关系备注") + private String remark; + + @AssertTrue(message = "关系失效时间不能早于生效时间") + @Schema(hidden = true) + public boolean isRelationTimeValid() { + return effectiveTime == null || expireTime == null || !expireTime.isBefore(effectiveTime); + } +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/user/SysUserRelationController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/user/SysUserRelationController.java index 03a540719..10f5e93ce 100755 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/user/SysUserRelationController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/user/SysUserRelationController.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationCreateUserDto; import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationCreateDto; +import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationUpdateDto; import com.bsd.pjcloud.admin.api.entity.user.SysUserRelationEntity; import com.bsd.pjcloud.admin.api.vo.user.SysUserRelationGraphVo; import com.bsd.pjcloud.admin.service.SysUserRelationService; @@ -35,7 +36,7 @@ public class SysUserRelationController { /** * 查询用户关系图谱 */ - @Operation(summary = "关系图谱", description = "以指定用户为根节点查询用户关系树") + @Operation(summary = "关系图谱", description = "查询指定用户所在的完整关系图谱,并按关系方向生成业务树") @GetMapping("/graph/{userId}") // @PreAuthorize("@pms.hasPermission('dzc_sysUserRelation_view')") public R getRelationGraph(@PathVariable Long userId) { @@ -64,4 +65,15 @@ public class SysUserRelationController { return R.ok(sysUserRelationService.createRelation(dto)); } + /** + * 修改已有用户关系 + */ + @Operation(summary = "修改用户关系", description = "修改关系类型、方向、生效时间及备注") + @SysLog("修改用户关系") + @PutMapping("/update") + // @PreAuthorize("@pms.hasPermission('dzc_sysUserRelation_edit')") + public R updateRelation(@Valid @RequestBody SysUserRelationUpdateDto dto) { + return R.ok(sysUserRelationService.updateRelation(dto)); + } + } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/user/SysUserRelationTypeMapper.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/user/SysUserRelationTypeMapper.java index 5822aa639..d75993c35 100755 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/user/SysUserRelationTypeMapper.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/user/SysUserRelationTypeMapper.java @@ -15,7 +15,10 @@ public interface SysUserRelationTypeMapper extends PjcloudBaseMapper scopePageList(Page page, SysUserRelationTypeDto dto, DataScope dataScope) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(ObjectUtil.isNotEmpty(dto.getRelationTypeId()), SysUserRelationTypeEntity::getRelationTypeId, dto.getRelationTypeId()) - .like(ObjectUtil.isNotEmpty(dto.getRelationName()), SysUserRelationTypeEntity::getRelationName, dto.getRelationName()); + .eq(ObjectUtil.isNotEmpty(dto.getCategory()), SysUserRelationTypeEntity::getCategory, dto.getCategory()) + .like(ObjectUtil.isNotEmpty(dto.getRelationName()), SysUserRelationTypeEntity::getRelationName, dto.getRelationName()) + .orderByDesc(SysUserRelationTypeEntity::getSortOrder) + .orderByDesc(SysUserRelationTypeEntity::getCreateTime); return selectPageByScope(page, wrapper, dataScope); } } \ No newline at end of file diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserRelationService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserRelationService.java index 255917f5c..9d71023cd 100755 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserRelationService.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserRelationService.java @@ -3,6 +3,7 @@ package com.bsd.pjcloud.admin.service; import com.baomidou.mybatisplus.extension.service.IService; import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationCreateUserDto; import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationCreateDto; +import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationUpdateDto; import com.bsd.pjcloud.admin.api.entity.user.SysUserRelationEntity; import com.bsd.pjcloud.admin.api.vo.user.SysUserRelationGraphVo; @@ -11,7 +12,7 @@ public interface SysUserRelationService extends IService /** * 查询用户关系图谱 * - * @param userId 根用户ID + * @param userId 图谱入口用户ID * @return 用户关系图谱 */ SysUserRelationGraphVo getRelationGraph(Long userId); @@ -31,4 +32,12 @@ public interface SysUserRelationService extends IService * @return 关系ID */ Long createRelation(SysUserRelationCreateDto dto); + + /** + * 修改已有用户关系 + * + * @param dto 关系信息 + * @return 关系ID + */ + Long updateRelation(SysUserRelationUpdateDto dto); } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/user/SysUserRelationServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/user/SysUserRelationServiceImpl.java index bc246aace..af80bf0e6 100755 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/user/SysUserRelationServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/user/SysUserRelationServiceImpl.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.bsd.pjcloud.admin.api.dto.UserDTO; import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationCreateUserDto; import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationCreateDto; +import com.bsd.pjcloud.admin.api.dto.user.SysUserRelationUpdateDto; import com.bsd.pjcloud.admin.api.entity.SysUser; import com.bsd.pjcloud.admin.api.entity.user.SysUserRelationEntity; import com.bsd.pjcloud.admin.api.entity.user.SysUserRelationTypeEntity; @@ -22,7 +23,11 @@ import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -48,10 +53,15 @@ public class SysUserRelationServiceImpl extends ServiceImpl first)); + Long rootUserId = selectRootUserId(userId, graphData, relationTypeMap); + List graphNodes = buildRelationTree(rootUserId, graphData); - Map nodeMap = graphData.nodes.stream() + Map nodeMap = graphNodes.stream() .map(node -> buildGraphNode(node, userMap, relationTypeMap)) .collect(Collectors.toMap(SysUserRelationGraphVo::getNodeId, Function.identity())); SysUserRelationGraphVo rootNode = nodeMap.get(TREE_ROOT_NODE_ID); - graphData.nodes.stream() + graphNodes.stream() .filter(node -> !Objects.equals(node.nodeId, TREE_ROOT_NODE_ID)) .forEach(node -> { SysUserRelationGraphVo parentNode = nodeMap.get(node.parentNodeId); @@ -93,7 +105,7 @@ public class SysUserRelationServiceImpl extends ServiceImpllambdaQuery() + .ne(excludeRelationId != null, SysUserRelationEntity::getRelationId, excludeRelationId) .eq(SysUserRelationEntity::getRelationTypeId, relationType.getRelationTypeId()) .and(wrapper -> wrapper .nested(current -> current.eq(SysUserRelationEntity::getSubjectUserId, subjectUserId) @@ -178,60 +214,162 @@ public class SysUserRelationServiceImpl extends ServiceImpl expandedUserIds = new HashSet<>(); - List currentLevel = List.of(new RelationNodeContext(TREE_ROOT_NODE_ID, rootUserId)); + Set currentLevel = Set.of(entryUserId); while (!currentLevel.isEmpty()) { - List expandableNodes = new ArrayList<>(); Set expandableUserIds = new LinkedHashSet<>(); - for (RelationNodeContext context : currentLevel) { - if (expandedUserIds.add(context.userId)) { - expandableNodes.add(context); - expandableUserIds.add(context.userId); + for (Long currentUserId : currentLevel) { + if (expandedUserIds.add(currentUserId)) { + expandableUserIds.add(currentUserId); } } - if (expandableNodes.isEmpty()) { + if (expandableUserIds.isEmpty()) { break; } - Map> relationMap = listRelations(expandableUserIds).stream() - .flatMap(relation -> Stream.of(relation.getSubjectUserId(), relation.getObjectUserId()) - .filter(Objects::nonNull) - .map(relationUserId -> Map.entry(relationUserId, relation))) - .collect(Collectors.groupingBy(Map.Entry::getKey, - Collectors.mapping(Map.Entry::getValue, Collectors.toList()))); - - List nextLevel = new ArrayList<>(); - for (RelationNodeContext context : expandableNodes) { - for (SysUserRelationEntity relation : relationMap.getOrDefault(context.userId, List.of())) { - boolean parentIsSubject = Objects.equals(context.userId, relation.getSubjectUserId()); - Long childUserId = parentIsSubject ? relation.getObjectUserId() : relation.getSubjectUserId(); - if (childUserId == null || Objects.equals(childUserId, context.userId)) { - continue; - } - if (!graphData.relationIds.add(relation.getRelationId())) { - continue; - } - - boolean repeated = !graphData.userIds.add(childUserId); - if (relation.getRelationTypeId() != null) { - graphData.relationTypeIds.add(relation.getRelationTypeId()); - } - graphData.nodes.add(RelationGraphNode.child( - relation.getRelationId(), context.nodeId, context.userId, childUserId, - relation, parentIsSubject, repeated)); - nextLevel.add(new RelationNodeContext(relation.getRelationId(), childUserId)); + Set nextLevel = new LinkedHashSet<>(); + for (SysUserRelationEntity relation : listRelations(expandableUserIds)) { + if (!graphData.relationIds.add(relation.getRelationId())) { + continue; } + graphData.relations.add(relation); + if (relation.getRelationTypeId() != null) { + graphData.relationTypeIds.add(relation.getRelationTypeId()); + } + Stream.of(relation.getSubjectUserId(), relation.getObjectUserId()) + .filter(Objects::nonNull) + .filter(graphData.userIds::add) + .forEach(nextLevel::add); } currentLevel = nextLevel; } return graphData; } + /** + * 根据有向关系的主语到对象方向选择业务根节点,优先选择可覆盖用户最多的顶层用户。 + * + * @param entryUserId 入口用户ID + * @param graphData 完整连通关系数据 + * @param relationTypeMap 关系类型映射 + * @return 业务根用户ID + */ + private Long selectRootUserId(Long entryUserId, RelationGraphData graphData, + Map relationTypeMap) { + Map> directedChildren = new HashMap<>(); + Set directedChildUserIds = new HashSet<>(); + for (SysUserRelationEntity relation : graphData.relations) { + SysUserRelationTypeEntity relationType = relationTypeMap.get(relation.getRelationTypeId()); + if (relationType == null || !"1".equals(relationType.getDirectedFlag())) { + continue; + } + directedChildren.computeIfAbsent(relation.getSubjectUserId(), key -> new LinkedHashSet<>()) + .add(relation.getObjectUserId()); + directedChildUserIds.add(relation.getObjectUserId()); + } + if (directedChildren.isEmpty()) { + return entryUserId; + } + + return graphData.userIds.stream() + .filter(candidateUserId -> !directedChildUserIds.contains(candidateUserId)) + .sorted(Comparator + .comparingInt((Long candidateUserId) -> countDirectedDescendants( + candidateUserId, directedChildren)).reversed() + .thenComparing(candidateUserId -> !Objects.equals(candidateUserId, entryUserId)) + .thenComparing(Comparator.naturalOrder())) + .findFirst() + .orElse(entryUserId); + } + + /** + * 统计指定用户沿有向关系可覆盖的用户数量,用于选择最合适的业务根节点。 + * + * @param rootUserId 候选根用户ID + * @param directedChildren 有向关系的下级用户映射 + * @return 可覆盖的用户数量,包含候选根用户自身 + */ + private int countDirectedDescendants(Long rootUserId, Map> directedChildren) { + Set visitedUserIds = new HashSet<>(); + Deque pendingUserIds = new ArrayDeque<>(); + pendingUserIds.add(rootUserId); + while (!pendingUserIds.isEmpty()) { + Long currentUserId = pendingUserIds.removeFirst(); + if (!visitedUserIds.add(currentUserId)) { + continue; + } + pendingUserIds.addAll(directedChildren.getOrDefault(currentUserId, Set.of())); + } + return visitedUserIds.size(); + } + + /** + * 从业务根节点开始生成树形结构,交叉关系或环中的重复用户只保留引用节点,不再继续展开。 + * + * @param rootUserId 业务根用户ID + * @param graphData 完整连通关系数据 + * @return 树形关系节点列表 + */ + private List buildRelationTree(Long rootUserId, RelationGraphData graphData) { + Map> relationMap = graphData.relations.stream() + .flatMap(relation -> Stream.of(relation.getSubjectUserId(), relation.getObjectUserId()) + .filter(Objects::nonNull) + .map(relationUserId -> Map.entry(relationUserId, relation))) + .collect(Collectors.groupingBy(Map.Entry::getKey, + Collectors.mapping(Map.Entry::getValue, Collectors.toList()))); + relationMap.values().forEach(relations -> relations.sort( + Comparator.comparing(SysUserRelationEntity::getRelationId))); + + List nodes = new ArrayList<>(); + nodes.add(RelationGraphNode.root(rootUserId)); + Set relationIds = new HashSet<>(); + Set discoveredUserIds = new HashSet<>(); + discoveredUserIds.add(rootUserId); + List currentLevel = List.of( + new RelationNodeContext(TREE_ROOT_NODE_ID, rootUserId)); + while (!currentLevel.isEmpty()) { + List nextLevel = new ArrayList<>(); + for (RelationNodeContext context : currentLevel) { + for (SysUserRelationEntity relation : relationMap.getOrDefault(context.userId, List.of())) { + if (!relationIds.add(relation.getRelationId())) { + continue; + } + boolean parentIsSubject = Objects.equals(context.userId, relation.getSubjectUserId()); + Long childUserId = parentIsSubject ? relation.getObjectUserId() : relation.getSubjectUserId(); + if (childUserId == null || Objects.equals(childUserId, context.userId)) { + continue; + } + + boolean repeated = !discoveredUserIds.add(childUserId); + nodes.add(RelationGraphNode.child( + relation.getRelationId(), context.nodeId, context.userId, childUserId, + relation, parentIsSubject, repeated)); + if (!repeated) { + nextLevel.add(new RelationNodeContext(relation.getRelationId(), childUserId)); + } + } + } + currentLevel = nextLevel; + } + return nodes; + } + + /** + * 查询与指定用户集合任意一端关联的全部有效关系。 + * + * @param userIds 用户ID集合 + * @return 用户关系列表 + */ private List listRelations(Set userIds) { return list(Wrappers.lambdaQuery() .and(wrapper -> wrapper.in(SysUserRelationEntity::getSubjectUserId, userIds) @@ -323,7 +461,7 @@ public class SysUserRelationServiceImpl extends ServiceImpl nodes = new ArrayList<>(); + private final List relations = new ArrayList<>(); private final Set userIds = new LinkedHashSet<>(); diff --git a/pjcloud-upms/pjcloud-upms-biz/src/test/java/com/bsd/pjcloud/admin/service/impl/user/SysUserRelationServiceImplTest.java b/pjcloud-upms/pjcloud-upms-biz/src/test/java/com/bsd/pjcloud/admin/service/impl/user/SysUserRelationServiceImplTest.java new file mode 100644 index 000000000..30d95e5e1 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/test/java/com/bsd/pjcloud/admin/service/impl/user/SysUserRelationServiceImplTest.java @@ -0,0 +1,95 @@ +package com.bsd.pjcloud.admin.service.impl.user; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.api.entity.user.SysUserRelationEntity; +import com.bsd.pjcloud.admin.api.entity.user.SysUserRelationTypeEntity; +import com.bsd.pjcloud.admin.api.vo.user.SysUserRelationGraphVo; +import com.bsd.pjcloud.admin.service.SysUserRelationTypeService; +import com.bsd.pjcloud.admin.service.SysUserService; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +class SysUserRelationServiceImplTest { + + private static final Long GUARDIAN_USER_ID = 2034824566572941314L; + + private static final Long FIRST_WARD_USER_ID = 2079103470488576001L; + + private static final Long SECOND_WARD_USER_ID = 2079129427022737410L; + + private static final Long RELATION_TYPE_ID = 2079044735024029698L; + + /** + * 验证从被监护人进入图谱时,仍以监护人为根节点并展示两个同级被监护人。 + */ + @Test + void shouldUseGuardianAsRootWhenGraphIsOpenedFromWard() { + SysUserRelationEntity firstRelation = createRelation( + 2079104982669385730L, FIRST_WARD_USER_ID); + SysUserRelationEntity secondRelation = createRelation( + 2079129636280758273L, SECOND_WARD_USER_ID); + + SysUserService sysUserService = mock(SysUserService.class); + SysUserRelationTypeService relationTypeService = mock(SysUserRelationTypeService.class); + SysUserRelationServiceImpl service = spy( + new SysUserRelationServiceImpl(sysUserService, relationTypeService)); + + when(sysUserService.getById(SECOND_WARD_USER_ID)).thenReturn(createUser(SECOND_WARD_USER_ID, "被监护人二")); + when(sysUserService.listByIds(any())).thenReturn(List.of( + createUser(GUARDIAN_USER_ID, "监护人"), + createUser(FIRST_WARD_USER_ID, "被监护人一"), + createUser(SECOND_WARD_USER_ID, "被监护人二"))); + when(relationTypeService.listByIds(any())).thenReturn(List.of(createGuardianRelationType())); + doReturn(List.of(secondRelation), List.of(firstRelation, secondRelation), List.of(firstRelation)) + .when(service).list(org.mockito.ArgumentMatchers.>any()); + + SysUserRelationGraphVo graph = service.getRelationGraph(SECOND_WARD_USER_ID); + + assertEquals(GUARDIAN_USER_ID, graph.getUserId()); + assertEquals(3, graph.getUserCount()); + assertEquals(2, graph.getRelationCount()); + assertEquals(Set.of(FIRST_WARD_USER_ID, SECOND_WARD_USER_ID), graph.getChildren().stream() + .map(SysUserRelationGraphVo::getUserId) + .collect(Collectors.toSet())); + assertFalse(graph.getChildren().stream().anyMatch(child -> !child.getChildren().isEmpty())); + } + + private SysUserRelationEntity createRelation(Long relationId, Long wardUserId) { + SysUserRelationEntity relation = new SysUserRelationEntity(); + relation.setRelationId(relationId); + relation.setSubjectUserId(GUARDIAN_USER_ID); + relation.setObjectUserId(wardUserId); + relation.setRelationTypeId(RELATION_TYPE_ID); + return relation; + } + + private SysUser createUser(Long userId, String name) { + SysUser user = new SysUser(); + user.setUserId(userId); + user.setName(name); + return user; + } + + private SysUserRelationTypeEntity createGuardianRelationType() { + SysUserRelationTypeEntity relationType = new SysUserRelationTypeEntity(); + relationType.setRelationTypeId(RELATION_TYPE_ID); + relationType.setRelationCode("GUARDIAN_WARD"); + relationType.setRelationName("监护关系"); + relationType.setSubjectRoleName("监护人"); + relationType.setObjectRoleName("被监护对象"); + relationType.setDirectedFlag("1"); + return relationType; + } +}