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 5c1c3c5..55e7564 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java @@ -37,8 +37,6 @@ import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.*; import org.springframework.web.multipart.MultipartFile; -import javax.persistence.criteria.Join; -import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.persistence.criteria.Subquery; @@ -267,11 +265,13 @@ public class SysUserService implements ISysUserService { public Page list(SysUserQuery query, Pageable pageable) { Specification spec = buildSpecification(query); Page page = sysUserRepository.findAll(spec, pageable); - // 把当前组维度的 sortNo 带到 Vo(仅在具体组上下文有意义;根节点/未选组时为 null) + // 把当前组维度的 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()), - query.getGroupId()); + groupIds); return page.map(u -> convertToVo(u, sortNoMap.get(u.getId()))); } return page.map(u -> convertToVo(u, null)); @@ -596,9 +596,10 @@ public class SysUserService implements ISysUserService { /** * 组装用户列表查询条件:组范围(含子组)+ 关键字(账号/姓名/身份证模糊、手机号精确)+ 启用状态。
* 排序按「是否选中具体组」分治:
- * - 选中具体组(groupId != null):JOIN 关联表,按组内 sort_no 正序(NULLS LAST),其次 user_id 正序; - * 含子组时同一用户可能命中多组,启用 distinct 去重(同时保证导出不去重重复行)。
- * - 根节点 / 未选组(groupId == null):不 JOIN 关联表,直接按主键 id 正序(聚簇索引顺序,无 filesort), + * - 选中具体组(groupId != null):用 EXISTS 子查询按组范围过滤成员,用「组内最小 sort_no」标量子查询排序 + * (NULLS LAST),其次 user_id 正序;不使用 JOIN+distinct,规避 MySQL 的 DISTINCT+ORDER BY 限制, + * 且含子组时同一用户命中多组也不会产生重复行。
+ * - 根节点 / 未选组(groupId == null):不关联组表,直接按主键 id 正序(聚簇索引顺序,无 filesort), * 平台总用户量不影响查询性能。 */ private Specification buildSpecification(SysUserQuery query) { @@ -606,18 +607,35 @@ public class SysUserService implements ISysUserService { List predicates = new ArrayList<>(); if (query.getGroupId() != null) { - // 具体组上下文:JOIN 关联表以按组内 sort_no 排序 + // 具体组上下文:按组内 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()); - Join rel = root.join("userGroupRelList", JoinType.INNER); - predicates.add(rel.get("groupId").in(groupIds)); - cbQuery.distinct(true); - // 排序规则:已设排序号者排最前(NULLS LAST),其次 sort_no 升序,最后 user_id 升序兜底 - // 注:Hibernate 5.6 的 AST 解析器不支持把 IS NULL 表达式直接放进 ORDER BY, - // 改用 CASE WHEN sort_no IS NULL THEN 1 ELSE 0 END(产出普通整数,语义等价 NULLS LAST)。 - // rel.get("sortNo") 返回 Path,直接作为 Expression 传入 orderBy 即可。 + + // 成员过滤:用户须归属于 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(rel.get("sortNo")), 1).otherwise(0)), - cb.asc(rel.get("sortNo")), + cb.asc(cb.selectCase().when(cb.isNull(sortSq), 1).otherwise(0)), + cb.asc(sortSq), cb.asc(root.get("id")) ); } else { @@ -645,16 +663,21 @@ public class SysUserService implements ISysUserService { } /** - * 批量取「用户在某组下的排序号」,用于列表 Vo 携带当前组维度 sortNo(仅具体组上下文调用)。 + * 批量取「用户在组范围内」的排序号(组内最小 sort_no),用于列表 Vo 携带当前组维度 sortNo。
+ * 与列表排序键(组内最小 sort_no)严格一致:含子组时 groupIds 为自身+子孙组,用户跨多组命中时取最小, + * 因此「排序顺序」与「展示的排序号」完全对应。 */ - private Map resolveUserSortNoInGroup(List userIds, Integer groupId) { + private Map resolveUserSortNoInGroup(List userIds, List groupIds) { Map sortNoMap = new HashMap<>(); - if (userIds.isEmpty()) { + if (userIds.isEmpty() || groupIds.isEmpty()) { return sortNoMap; } - List rels = sysUserGroupRelRepository.findByUserIdInAndGroupId(userIds, groupId); - for (SysUserGroupRel rel : rels) { - sortNoMap.put(rel.getUserId(), rel.getSortNo()); + 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; }