This commit is contained in:
@@ -3,8 +3,6 @@ package com.yunhai.userbase.repository;
|
|||||||
import com.yunhai.userbase.entity.SysUserGroupRel;
|
import com.yunhai.userbase.entity.SysUserGroupRel;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
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.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -26,15 +24,6 @@ public interface SysUserGroupRelRepository extends JpaRepository<SysUserGroupRel
|
|||||||
|
|
||||||
List<SysUserGroupRel> findByUserIdInAndGroupId(List<Integer> userIds, Integer groupId);
|
List<SysUserGroupRel> findByUserIdInAndGroupId(List<Integer> userIds, Integer groupId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 取「用户在指定组范围内」的最小 sort_no(按 userId 聚合),用于列表 Vo 携带排序号。<br>
|
|
||||||
* 与列表排序(组内最小 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<Object[]> findMinSortNoByUserIdInAndGroupIdIn(@Param("userIds") List<Integer> userIds,
|
|
||||||
@Param("groupIds") List<Integer> groupIds);
|
|
||||||
|
|
||||||
Optional<SysUserGroupRel> findByUserIdAndGroupId(Integer userId, Integer groupId);
|
Optional<SysUserGroupRel> findByUserIdAndGroupId(Integer userId, Integer groupId);
|
||||||
|
|
||||||
List<SysUserGroupRel> findByGroupId(Integer groupId);
|
List<SysUserGroupRel> findByGroupId(Integer groupId);
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
package com.yunhai.userbase.repository;
|
package com.yunhai.userbase.repository;
|
||||||
|
|
||||||
import com.yunhai.userbase.entity.SysUser;
|
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.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
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
|
* @author chenx
|
||||||
@@ -20,4 +26,39 @@ public interface SysUserRepository extends JpaRepository<SysUser, Integer>, JpaS
|
|||||||
|
|
||||||
/** 按手机号查用户,供用户中心组装 BoaUser。 */
|
/** 按手机号查用户,供用户中心组装 BoaUser。 */
|
||||||
SysUser findByPhone(String phone);
|
SysUser findByPhone(String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组上下文列表专用分页(原生 SQL)。<br>
|
||||||
|
* 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 标量子查询的限制。<br>
|
||||||
|
* 入参 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<Object[]> findGroupUserPage(@Param("groupIds") List<Integer> groupIds,
|
||||||
|
@Param("name") String name,
|
||||||
|
@Param("disabled") Integer disabled,
|
||||||
|
Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
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.domain.Pageable;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
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.Predicate;
|
||||||
import javax.persistence.criteria.Root;
|
import javax.persistence.criteria.Root;
|
||||||
import javax.persistence.criteria.Subquery;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@@ -263,20 +264,64 @@ public class SysUserService implements ISysUserService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<SysUserVo> list(SysUserQuery query, Pageable pageable) {
|
public Page<SysUserVo> 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<SysUser> spec = buildSpecification(query);
|
Specification<SysUser> spec = buildSpecification(query);
|
||||||
Page<SysUser> page = sysUserRepository.findAll(spec, pageable);
|
Page<SysUser> page = sysUserRepository.findAll(spec, pageable);
|
||||||
// 把当前组维度的 sortNo 带到 Vo(仅在具体组上下文有意义;根节点/未选组时为 null)。
|
|
||||||
// 组范围须与列表排序一致:含子组时取自身+子孙组范围内的最小 sort_no,避免「排序按范围、展示按单组」的不一致。
|
|
||||||
if (query.getGroupId() != null) {
|
|
||||||
List<Integer> groupIds = resolveGroupIds(query.getGroupId(), query.getWithSub());
|
|
||||||
Map<Integer, Integer> 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));
|
return page.map(u -> convertToVo(u, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组上下文列表:原生 SQL 分页。<br>
|
||||||
|
* 思路: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<SysUserVo> listByGroup(SysUserQuery query, Pageable pageable) {
|
||||||
|
List<Integer> groupIds = resolveGroupIds(query.getGroupId(), query.getWithSub());
|
||||||
|
String name = StringUtils.isNotBlank(query.getName()) ? query.getName().trim() : null;
|
||||||
|
Integer disabledKey = query.getDisabled();
|
||||||
|
|
||||||
|
Page<Object[]> idPage = sysUserRepository.findGroupUserPage(
|
||||||
|
groupIds, name, disabledKey,
|
||||||
|
PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()));
|
||||||
|
|
||||||
|
List<Object[]> rows = idPage.getContent();
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
return new PageImpl<>(Collections.emptyList(), pageable, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保持 DB 返回的排序顺序,并记录每个用户的「组范围内最小 sort_no」
|
||||||
|
List<Integer> orderedIds = new ArrayList<>(rows.size());
|
||||||
|
Map<Integer, Integer> 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<SysUser> users = sysUserRepository.findAllById(orderedIds);
|
||||||
|
Map<Integer, SysUser> userMap = users.stream()
|
||||||
|
.collect(Collectors.toMap(SysUser::getId, u -> u, (a, b) -> a));
|
||||||
|
|
||||||
|
List<SysUserVo> 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
|
@Override
|
||||||
public List<Integer> findGroupIdsByUserId(Integer userId) {
|
public List<Integer> findGroupIdsByUserId(Integer userId) {
|
||||||
return sysUserGroupRelRepository.findByUserId(userId).stream()
|
return sysUserGroupRelRepository.findByUserId(userId).stream()
|
||||||
@@ -594,54 +639,17 @@ public class SysUserService implements ISysUserService {
|
|||||||
// ===================== 私有工具 =====================
|
// ===================== 私有工具 =====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组装用户列表查询条件:组范围(含子组)+ 关键字(账号/姓名/身份证模糊、手机号精确)+ 启用状态。<br>
|
* 组装「根节点 / 未选组」上下文的用户列表查询条件:关键字(账号/姓名/身份证模糊、手机号精确)+ 启用状态,
|
||||||
* 排序按「是否选中具体组」分治:<br>
|
* 按主键 id 升序(聚簇索引顺序,无 filesort)。<br>
|
||||||
* - 选中具体组(groupId != null):用 EXISTS 子查询按组范围过滤成员,用「组内最小 sort_no」标量子查询排序
|
* 注意:选中具体组的上下文不走 Specification,改由 {@link #listByGroup} 用原生 SQL 分页,
|
||||||
* (NULLS LAST),其次 user_id 正序;不使用 JOIN+distinct,规避 MySQL 的 DISTINCT+ORDER BY 限制,
|
* 以规避 MySQL 的 DISTINCT+ORDER BY 限制与 Hibernate 不支持 ORDER BY 标量子查询的限制。
|
||||||
* 且含子组时同一用户命中多组也不会产生重复行。<br>
|
|
||||||
* - 根节点 / 未选组(groupId == null):不关联组表,直接按主键 id 正序(聚簇索引顺序,无 filesort),
|
|
||||||
* 平台总用户量不影响查询性能。
|
|
||||||
*/
|
*/
|
||||||
private Specification<SysUser> buildSpecification(SysUserQuery query) {
|
private Specification<SysUser> buildSpecification(SysUserQuery query) {
|
||||||
return (root, cbQuery, cb) -> {
|
return (root, cbQuery, cb) -> {
|
||||||
List<Predicate> predicates = new ArrayList<>();
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
|
|
||||||
if (query.getGroupId() != null) {
|
// 根节点 / 未选组:无组上下文,按主键 id 升序
|
||||||
// 具体组上下文:按组内 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<Integer> groupIds = resolveGroupIds(query.getGroupId(), query.getWithSub());
|
|
||||||
|
|
||||||
// 成员过滤:用户须归属于 groupIds 中的某一组
|
|
||||||
Subquery<Integer> memberSq = cbQuery.subquery(Integer.class);
|
|
||||||
Root<SysUserGroupRel> 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<Integer> sortSq = cbQuery.subquery(Integer.class);
|
|
||||||
Root<SysUserGroupRel> sortRoot = sortSq.from(SysUserGroupRel.class);
|
|
||||||
sortSq.select(cb.min(sortRoot.<Integer>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")));
|
cbQuery.orderBy(cb.asc(root.get("id")));
|
||||||
}
|
|
||||||
|
|
||||||
// 关键字:账号/姓名/身份证模糊,手机号精确
|
// 关键字:账号/姓名/身份证模糊,手机号精确
|
||||||
if (StringUtils.isNotBlank(query.getName())) {
|
if (StringUtils.isNotBlank(query.getName())) {
|
||||||
@@ -663,26 +671,8 @@ public class SysUserService implements ISysUserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量取「用户在组范围内」的排序号(组内最小 sort_no),用于列表 Vo 携带当前组维度 sortNo。<br>
|
* 依据 withSub 决定组范围:1=含所有子孙组,否则仅当前组。
|
||||||
* 与列表排序键(组内最小 sort_no)严格一致:含子组时 groupIds 为自身+子孙组,用户跨多组命中时取最小,
|
|
||||||
* 因此「排序顺序」与「展示的排序号」完全对应。
|
|
||||||
*/
|
*/
|
||||||
private Map<Integer, Integer> resolveUserSortNoInGroup(List<Integer> userIds, List<Integer> groupIds) {
|
|
||||||
Map<Integer, Integer> sortNoMap = new HashMap<>();
|
|
||||||
if (userIds.isEmpty() || groupIds.isEmpty()) {
|
|
||||||
return sortNoMap;
|
|
||||||
}
|
|
||||||
List<Object[]> 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<Integer> resolveGroupIds(Integer groupId, Integer withSub) {
|
private List<Integer> resolveGroupIds(Integer groupId, Integer withSub) {
|
||||||
if (withSub != null && withSub == 1) {
|
if (withSub != null && withSub == 1) {
|
||||||
return sysGroupService.listSelfAndDescendantIds(groupId);
|
return sysGroupService.listSelfAndDescendantIds(groupId);
|
||||||
|
|||||||
Reference in New Issue
Block a user