diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java index ceff7c7..0a53aff 100644 --- a/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java +++ b/src/main/java/com/yunhai/userbase/repository/SysUserGroupRelRepository.java @@ -3,8 +3,6 @@ package com.yunhai.userbase.repository; import com.yunhai.userbase.entity.SysUserGroupRel; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -26,15 +24,6 @@ public interface SysUserGroupRelRepository extends JpaRepository findByUserIdInAndGroupId(List userIds, Integer groupId); - /** - * 取「用户在指定组范围内」的最小 sort_no(按 userId 聚合),用于列表 Vo 携带排序号。
- * 与列表排序(组内最小 sort_no)保持一致;组范围内无记录则返回空。 - */ - @Query("SELECT r.userId, MIN(r.sortNo) FROM SysUserGroupRel r " + - "WHERE r.userId IN :userIds AND r.groupId IN :groupIds GROUP BY r.userId") - List findMinSortNoByUserIdInAndGroupIdIn(@Param("userIds") List userIds, - @Param("groupIds") List groupIds); - Optional findByUserIdAndGroupId(Integer userId, Integer groupId); List findByGroupId(Integer groupId); diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java index f885532..16b5f86 100644 --- a/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java +++ b/src/main/java/com/yunhai/userbase/repository/SysUserRepository.java @@ -1,8 +1,14 @@ package com.yunhai.userbase.repository; import com.yunhai.userbase.entity.SysUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; /** * @author chenx @@ -20,4 +26,39 @@ public interface SysUserRepository extends JpaRepository, JpaS /** 按手机号查用户,供用户中心组装 BoaUser。 */ SysUser findByPhone(String phone); + + /** + * 组上下文列表专用分页(原生 SQL)。
+ * INNER JOIN 关联表后按 user_id 聚合(GROUP BY u.id),以「组内最小 sort_no」为排序键 + * (NULLS LAST + sort_no 升序 + user_id 兜底升序)。GROUP BY 天然去重,规避 MySQL 的 + * DISTINCT+ORDER BY 限制与 Hibernate 不支持 ORDER BY 标量子查询的限制。
+ * 入参 name / disabled 为可空:为空(NULL)时该过滤条件整体跳过。 + */ + @Query(value = + "SELECT u.id AS userId, MIN(r.sort_no) AS minSortNo " + + "FROM sys_user u " + + "INNER JOIN sys_user_group_rel r ON r.user_id = u.id " + + "WHERE r.group_id IN (:groupIds) " + + " AND (:name IS NULL OR u.account LIKE CONCAT('%', :name, '%') " + + " OR u.real_name LIKE CONCAT('%', :name, '%') " + + " OR u.idcard LIKE CONCAT('%', :name, '%') " + + " OR u.phone = :name) " + + " AND (:disabled IS NULL OR u.disabled = :disabled) " + + "GROUP BY u.id " + + "ORDER BY CASE WHEN MIN(r.sort_no) IS NULL THEN 1 ELSE 0 END, MIN(r.sort_no) ASC, u.id ASC", + countQuery = + "SELECT COUNT(DISTINCT u.id) " + + "FROM sys_user u " + + "INNER JOIN sys_user_group_rel r ON r.user_id = u.id " + + "WHERE r.group_id IN (:groupIds) " + + " AND (:name IS NULL OR u.account LIKE CONCAT('%', :name, '%') " + + " OR u.real_name LIKE CONCAT('%', :name, '%') " + + " OR u.idcard LIKE CONCAT('%', :name, '%') " + + " OR u.phone = :name) " + + " AND (:disabled IS NULL OR u.disabled = :disabled)", + nativeQuery = true) + Page findGroupUserPage(@Param("groupIds") List groupIds, + @Param("name") String name, + @Param("disabled") Integer disabled, + Pageable pageable); } diff --git a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java index 55e7564..81667ea 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java @@ -28,6 +28,8 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.security.crypto.password.PasswordEncoder; @@ -39,7 +41,6 @@ import org.springframework.web.multipart.MultipartFile; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; -import javax.persistence.criteria.Subquery; import java.io.IOException; import java.io.OutputStream; @@ -263,20 +264,64 @@ public class SysUserService implements ISysUserService { @Override public Page list(SysUserQuery query, Pageable pageable) { + // 组上下文(选中具体组):走原生 SQL 按「组内最小 sort_no」排序的专用分页, + // 规避 MySQL 的 DISTINCT+ORDER BY 限制以及 Hibernate 不支持 ORDER BY 标量子查询的限制。 + if (query.getGroupId() != null) { + return listByGroup(query, pageable); + } + // 根节点 / 未选组:用 Specification 过滤,按主键 id 升序(聚簇索引顺序,无 filesort)。 Specification spec = buildSpecification(query); Page page = sysUserRepository.findAll(spec, pageable); - // 把当前组维度的 sortNo 带到 Vo(仅在具体组上下文有意义;根节点/未选组时为 null)。 - // 组范围须与列表排序一致:含子组时取自身+子孙组范围内的最小 sort_no,避免「排序按范围、展示按单组」的不一致。 - if (query.getGroupId() != null) { - List groupIds = resolveGroupIds(query.getGroupId(), query.getWithSub()); - Map sortNoMap = resolveUserSortNoInGroup( - page.getContent().stream().map(SysUser::getId).collect(Collectors.toList()), - groupIds); - return page.map(u -> convertToVo(u, sortNoMap.get(u.getId()))); - } return page.map(u -> convertToVo(u, null)); } + /** + * 组上下文列表:原生 SQL 分页。
+ * 思路:INNER JOIN 关联表后按 user_id 聚合(GROUP BY u.id),用 MIN(sort_no) 作为排序键 + * (NULLS LAST + sort_no 升序 + user_id 兜底升序)。GROUP BY 天然去重,既无 DISTINCT+ORDER BY 冲突, + * 也无 ORDER BY 标量子查询(Hibernate 5.6 不支持),分页/排序语义与「组内自定义排序」完全一致。 + * 同页的 (userId, minSortNo) 一并查出,省去二次查询。 + */ + private Page listByGroup(SysUserQuery query, Pageable pageable) { + List groupIds = resolveGroupIds(query.getGroupId(), query.getWithSub()); + String name = StringUtils.isNotBlank(query.getName()) ? query.getName().trim() : null; + Integer disabledKey = query.getDisabled(); + + Page idPage = sysUserRepository.findGroupUserPage( + groupIds, name, disabledKey, + PageRequest.of(pageable.getPageNumber(), pageable.getPageSize())); + + List rows = idPage.getContent(); + if (rows.isEmpty()) { + return new PageImpl<>(Collections.emptyList(), pageable, 0); + } + + // 保持 DB 返回的排序顺序,并记录每个用户的「组范围内最小 sort_no」 + List orderedIds = new ArrayList<>(rows.size()); + Map sortNoMap = new LinkedHashMap<>(); + for (Object[] row : rows) { + Integer userId = ((Number) row[0]).intValue(); + Object minObj = row[1]; // MIN(sort_no) 可能为空(该用户在组范围内均未设置排序号) + Integer sortNo = minObj == null ? null : ((Number) minObj).intValue(); + orderedIds.add(userId); + sortNoMap.put(userId, sortNo); + } + + // 按有序 ID 取实体(findAllById 返回顺序不可靠,需按 orderedIds 重排) + List users = sysUserRepository.findAllById(orderedIds); + Map userMap = users.stream() + .collect(Collectors.toMap(SysUser::getId, u -> u, (a, b) -> a)); + + List voList = new ArrayList<>(orderedIds.size()); + for (Integer id : orderedIds) { + SysUser u = userMap.get(id); + if (u != null) { + voList.add(convertToVo(u, sortNoMap.get(id))); + } + } + return new PageImpl<>(voList, pageable, idPage.getTotalElements()); + } + @Override public List findGroupIdsByUserId(Integer userId) { return sysUserGroupRelRepository.findByUserId(userId).stream() @@ -594,54 +639,17 @@ public class SysUserService implements ISysUserService { // ===================== 私有工具 ===================== /** - * 组装用户列表查询条件:组范围(含子组)+ 关键字(账号/姓名/身份证模糊、手机号精确)+ 启用状态。
- * 排序按「是否选中具体组」分治:
- * - 选中具体组(groupId != null):用 EXISTS 子查询按组范围过滤成员,用「组内最小 sort_no」标量子查询排序 - * (NULLS LAST),其次 user_id 正序;不使用 JOIN+distinct,规避 MySQL 的 DISTINCT+ORDER BY 限制, - * 且含子组时同一用户命中多组也不会产生重复行。
- * - 根节点 / 未选组(groupId == null):不关联组表,直接按主键 id 正序(聚簇索引顺序,无 filesort), - * 平台总用户量不影响查询性能。 + * 组装「根节点 / 未选组」上下文的用户列表查询条件:关键字(账号/姓名/身份证模糊、手机号精确)+ 启用状态, + * 按主键 id 升序(聚簇索引顺序,无 filesort)。
+ * 注意:选中具体组的上下文不走 Specification,改由 {@link #listByGroup} 用原生 SQL 分页, + * 以规避 MySQL 的 DISTINCT+ORDER BY 限制与 Hibernate 不支持 ORDER BY 标量子查询的限制。 */ private Specification buildSpecification(SysUserQuery query) { return (root, cbQuery, cb) -> { List predicates = new ArrayList<>(); - if (query.getGroupId() != null) { - // 具体组上下文:按组内 sort_no 排序、按组范围过滤成员。 - // 注意:不能 root.join(rel) + distinct 后再 ORDER BY rel.sortNo —— - // MySQL 在 DISTINCT 下禁止 ORDER BY 未选中列(报 "incompatible with DISTINCT"), - // 且 WITH SUB 时同一用户命中多组会因 JOIN 产生重复行。 - // 改用「成员资格 EXISTS 子查询」过滤 + 「组内最小 sort_no 标量子查询」排序: - // 不产生重复行,也规避 DISTINCT+ORDER BY 限制,分页/排序语义保持不变。 - List groupIds = resolveGroupIds(query.getGroupId(), query.getWithSub()); - - // 成员过滤:用户须归属于 groupIds 中的某一组 - Subquery memberSq = cbQuery.subquery(Integer.class); - Root memberRoot = memberSq.from(SysUserGroupRel.class); - memberSq.select(memberRoot.get("userId")); - memberSq.where( - memberRoot.get("groupId").in(groupIds), - cb.equal(memberRoot.get("userId"), root.get("id")) - ); - predicates.add(cb.exists(memberSq)); - - // 排序:取该用户在组范围内的「最小 sort_no」作为排序键(NULLS LAST),其次 user_id 兜底 - Subquery sortSq = cbQuery.subquery(Integer.class); - Root sortRoot = sortSq.from(SysUserGroupRel.class); - sortSq.select(cb.min(sortRoot.get("sortNo"))); - sortSq.where( - sortRoot.get("groupId").in(groupIds), - cb.equal(sortRoot.get("userId"), root.get("id")) - ); - cbQuery.orderBy( - cb.asc(cb.selectCase().when(cb.isNull(sortSq), 1).otherwise(0)), - cb.asc(sortSq), - cb.asc(root.get("id")) - ); - } else { - // 根节点 / 未选组:无组上下文,按主键 id 升序(聚簇索引顺序,不产生 filesort) - cbQuery.orderBy(cb.asc(root.get("id"))); - } + // 根节点 / 未选组:无组上下文,按主键 id 升序 + cbQuery.orderBy(cb.asc(root.get("id"))); // 关键字:账号/姓名/身份证模糊,手机号精确 if (StringUtils.isNotBlank(query.getName())) { @@ -663,26 +671,8 @@ public class SysUserService implements ISysUserService { } /** - * 批量取「用户在组范围内」的排序号(组内最小 sort_no),用于列表 Vo 携带当前组维度 sortNo。
- * 与列表排序键(组内最小 sort_no)严格一致:含子组时 groupIds 为自身+子孙组,用户跨多组命中时取最小, - * 因此「排序顺序」与「展示的排序号」完全对应。 + * 依据 withSub 决定组范围:1=含所有子孙组,否则仅当前组。 */ - private Map resolveUserSortNoInGroup(List userIds, List groupIds) { - Map sortNoMap = new HashMap<>(); - if (userIds.isEmpty() || groupIds.isEmpty()) { - return sortNoMap; - } - List rows = sysUserGroupRelRepository.findMinSortNoByUserIdInAndGroupIdIn(userIds, groupIds); - for (Object[] row : rows) { - Integer userId = (Integer) row[0]; - Object minObj = row[1]; // MIN(sort_no) 可能为空(该用户范围内均未设置排序号) - Integer sortNo = minObj == null ? null : ((Number) minObj).intValue(); - sortNoMap.put(userId, sortNo); - } - return sortNoMap; - } - - /** 依据 withSub 决定组范围:1=含所有子孙组,否则仅当前组。 */ private List resolveGroupIds(Integer groupId, Integer withSub) { if (withSub != null && withSub == 1) { return sysGroupService.listSelfAndDescendantIds(groupId);