150 changed files with 324 additions and 308 deletions
@ -0,0 +1,24 @@ |
|||||
|
package com.crm.auth.service; |
||||
|
|
||||
|
import com.crm.auth.domain.dto.UserListDTO; |
||||
|
import com.crm.auth.domain.dto.UserStatsVO; |
||||
|
import com.crm.auth.domain.param.UserPageParam; |
||||
|
import com.crm.base.domain.result.PageResult; |
||||
|
|
||||
|
/** |
||||
|
* 用户管理-查询侧(CQRS-lite):用户列表分页 + 指标卡统计。 |
||||
|
* <p>从 {@link IAuthUserService} 拆出,写操作(分配部门/角色)留在原接口, |
||||
|
* 读模型拼装在此独立演化,避免 Divergent Change。</p> |
||||
|
*/ |
||||
|
public interface IUserQueryService { |
||||
|
|
||||
|
/** |
||||
|
* 用户管理-用户列表分页(部门子树 × 在职状态 × 关键词 × 最后登录时间),反范式部门名/角色名。 |
||||
|
*/ |
||||
|
PageResult<UserListDTO> pageUserList(UserPageParam param); |
||||
|
|
||||
|
/** |
||||
|
* 用户管理-指标卡统计(部门总数/在职/离职/待分配),跟随部门子树,与列表过滤解耦。 |
||||
|
*/ |
||||
|
UserStatsVO stats(Long deptId); |
||||
|
} |
||||
@ -0,0 +1,216 @@ |
|||||
|
package com.crm.auth.service.impl; |
||||
|
|
||||
|
import cn.hutool.core.bean.BeanUtil; |
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
||||
|
import com.baomidou.mybatisplus.core.metadata.IPage; |
||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
||||
|
import com.crm.auth.domain.dto.UserListDTO; |
||||
|
import com.crm.auth.domain.dto.UserStatsVO; |
||||
|
import com.crm.auth.domain.entity.AuthUser; |
||||
|
import com.crm.auth.domain.entity.SysDept; |
||||
|
import com.crm.auth.domain.entity.SysRole; |
||||
|
import com.crm.auth.domain.entity.SysUserDept; |
||||
|
import com.crm.auth.domain.entity.SysUserRole; |
||||
|
import com.crm.auth.domain.param.UserPageParam; |
||||
|
import com.crm.auth.mapper.AuthUserMapper; |
||||
|
import com.crm.auth.mapper.SysDeptMapper; |
||||
|
import com.crm.auth.mapper.SysRoleMapper; |
||||
|
import com.crm.auth.mapper.SysUserDeptMapper; |
||||
|
import com.crm.auth.mapper.SysUserRoleMapper; |
||||
|
import com.crm.auth.service.ISysDeptService; |
||||
|
import com.crm.auth.service.IUserQueryService; |
||||
|
import com.crm.base.domain.result.PageResult; |
||||
|
import com.crm.base.utils.PageConverter; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.time.LocalDateTime; |
||||
|
import java.time.LocalTime; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Collections; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
/** |
||||
|
* 用户管理-查询侧实现(CQRS-lite):从 {@link AuthUserServiceImpl} 拆出的读模型拼装逻辑。 |
||||
|
* <p>不继承 BaseServiceImpl,直接注入 AuthUserMapper 做分页/计数/列表查询。 |
||||
|
* 写操作(assignDepts/assignRoles)仍在 {@link AuthUserServiceImpl}。</p> |
||||
|
*/ |
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
public class UserQueryServiceImpl implements IUserQueryService { |
||||
|
|
||||
|
private final AuthUserMapper authUserMapper; |
||||
|
private final SysUserDeptMapper sysUserDeptMapper; |
||||
|
private final SysDeptMapper sysDeptMapper; |
||||
|
private final SysUserRoleMapper sysUserRoleMapper; |
||||
|
private final SysRoleMapper sysRoleMapper; |
||||
|
private final ISysDeptService sysDeptService; |
||||
|
|
||||
|
private static final String STATUS_ACTIVE = "active"; |
||||
|
private static final String STATUS_RESIGNED = "resigned"; |
||||
|
private static final String STATUS_UNASSIGNED = "unassigned"; |
||||
|
|
||||
|
@Override |
||||
|
public PageResult<UserListDTO> pageUserList(UserPageParam param) { |
||||
|
Page<AuthUser> mpPage = PageConverter.toMpPage(param); |
||||
|
|
||||
|
// 部门子树 → 部门集合∩子树的候选用户 id(deptId 为空则不限部门)
|
||||
|
List<Long> scopedUserIds = resolveScopedUserIds(param.getDeptId()); |
||||
|
if (scopedUserIds != null && scopedUserIds.isEmpty()) { |
||||
|
return emptyUserDTOPage(mpPage); |
||||
|
} |
||||
|
|
||||
|
String status = param.getEmploymentStatus(); |
||||
|
boolean unassigned = STATUS_UNASSIGNED.equalsIgnoreCase(status); |
||||
|
boolean active = unassigned || STATUS_ACTIVE.equalsIgnoreCase(status); |
||||
|
boolean resigned = STATUS_RESIGNED.equalsIgnoreCase(status); |
||||
|
|
||||
|
// 预先求值,避免 MP 条件参数即时求值 null 字段导致 NPE
|
||||
|
LocalDateTime loginFromStart = param.getLoginFrom() != null |
||||
|
? param.getLoginFrom().atStartOfDay() : null; |
||||
|
LocalDateTime loginToEnd = param.getLoginTo() != null |
||||
|
? param.getLoginTo().atTime(LocalTime.MAX) : null; |
||||
|
|
||||
|
LambdaQueryWrapper<AuthUser> wrapper = new LambdaQueryWrapper<AuthUser>() |
||||
|
.in(scopedUserIds != null, AuthUser::getId, scopedUserIds) |
||||
|
.eq(active, AuthUser::getEmploymentStatus, STATUS_ACTIVE) |
||||
|
.eq(resigned, AuthUser::getEmploymentStatus, STATUS_RESIGNED) |
||||
|
.and(StrUtil.isNotBlank(param.getKeyword()), |
||||
|
w -> w.like(AuthUser::getUsername, param.getKeyword()) |
||||
|
.or().like(AuthUser::getAccount, param.getKeyword()) |
||||
|
.or().like(AuthUser::getMobile, param.getKeyword())) |
||||
|
.ge(loginFromStart != null, AuthUser::getLastLoginTime, loginFromStart) |
||||
|
.le(loginToEnd != null, AuthUser::getLastLoginTime, loginToEnd) |
||||
|
// 待分配 = 在职且无任何角色(NOT IN sys_user_role)
|
||||
|
.notInSql(unassigned, AuthUser::getId, "SELECT user_id FROM sys_user_role"); |
||||
|
Page<AuthUser> result = authUserMapper.selectPage(mpPage, wrapper); |
||||
|
|
||||
|
List<AuthUser> records = result.getRecords(); |
||||
|
if (records.isEmpty()) { |
||||
|
return emptyUserDTOPage(result); |
||||
|
} |
||||
|
|
||||
|
// 反范式部门名/角色名:批量查、组装,避免 N+1
|
||||
|
List<Long> pageUserIds = records.stream().map(AuthUser::getId).toList(); |
||||
|
Map<Long, List<Long>> partTimeByUser = sysUserDeptMapper.selectList( |
||||
|
new LambdaQueryWrapper<SysUserDept>() |
||||
|
.in(SysUserDept::getUserId, pageUserIds)) |
||||
|
.stream().collect(Collectors.groupingBy(SysUserDept::getUserId, |
||||
|
Collectors.mapping(SysUserDept::getDeptId, Collectors.toList()))); |
||||
|
Map<Long, List<Long>> roleIdsByUser = sysUserRoleMapper.selectList( |
||||
|
new LambdaQueryWrapper<SysUserRole>() |
||||
|
.in(SysUserRole::getUserId, pageUserIds)) |
||||
|
.stream().collect(Collectors.groupingBy(SysUserRole::getUserId, |
||||
|
Collectors.mapping(SysUserRole::getRoleId, Collectors.toList()))); |
||||
|
|
||||
|
Set<Long> deptIds = new HashSet<>(); |
||||
|
for (AuthUser u : records) { |
||||
|
if (u.getDeptId() != null) deptIds.add(u.getDeptId()); |
||||
|
} |
||||
|
partTimeByUser.values().forEach(deptIds::addAll); |
||||
|
Map<Long, String> deptNameMap = deptIds.isEmpty() |
||||
|
? Collections.emptyMap() |
||||
|
: sysDeptMapper.selectBatchIds(deptIds).stream() |
||||
|
.collect(Collectors.toMap(SysDept::getId, SysDept::getDeptName)); |
||||
|
|
||||
|
Set<Long> roleIds = new HashSet<>(); |
||||
|
roleIdsByUser.values().forEach(roleIds::addAll); |
||||
|
Map<Long, String> roleNameMap = roleIds.isEmpty() |
||||
|
? Collections.emptyMap() |
||||
|
: sysRoleMapper.selectBatchIds(roleIds).stream() |
||||
|
.collect(Collectors.toMap(SysRole::getId, SysRole::getRoleName)); |
||||
|
|
||||
|
return new PageResult<AuthUser>(result).convert(u -> |
||||
|
assembleUserListDTO(u, partTimeByUser, roleIdsByUser, deptNameMap, roleNameMap)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public UserStatsVO stats(Long deptId) { |
||||
|
// 部门子树(含自身);null 退化为全公司
|
||||
|
int deptCount; |
||||
|
if (deptId == null) { |
||||
|
deptCount = Math.toIntExact(sysDeptService.count()); |
||||
|
} else { |
||||
|
deptCount = sysDeptService.getChildDeptIds(deptId).size(); |
||||
|
} |
||||
|
|
||||
|
List<Long> scopedUserIds = resolveScopedUserIds(deptId); |
||||
|
if (scopedUserIds != null && scopedUserIds.isEmpty()) { |
||||
|
return new UserStatsVO(deptCount, 0L, 0L, 0L); |
||||
|
} |
||||
|
|
||||
|
long activeCount = authUserMapper.selectCount(new LambdaQueryWrapper<AuthUser>() |
||||
|
.in(scopedUserIds != null, AuthUser::getId, scopedUserIds) |
||||
|
.eq(AuthUser::getEmploymentStatus, STATUS_ACTIVE)); |
||||
|
long resignedCount = authUserMapper.selectCount(new LambdaQueryWrapper<AuthUser>() |
||||
|
.in(scopedUserIds != null, AuthUser::getId, scopedUserIds) |
||||
|
.eq(AuthUser::getEmploymentStatus, STATUS_RESIGNED)); |
||||
|
long unassignedCount = authUserMapper.selectCount(new LambdaQueryWrapper<AuthUser>() |
||||
|
.in(scopedUserIds != null, AuthUser::getId, scopedUserIds) |
||||
|
.eq(AuthUser::getEmploymentStatus, STATUS_ACTIVE) |
||||
|
.notInSql(AuthUser::getId, "SELECT user_id FROM sys_user_role")); |
||||
|
return new UserStatsVO(deptCount, activeCount, resignedCount, unassignedCount); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解析部门子树内"部门集合∩子树"的用户 id(主部门 dept_id ∪ 兼职 sys_user_dept.dept_id 命中子树者)。 |
||||
|
* deptId 为 null 时返回 null(表示不限部门);命中空集返回 emptyList。 |
||||
|
*/ |
||||
|
private List<Long> resolveScopedUserIds(Long deptId) { |
||||
|
if (deptId == null) { |
||||
|
return null; |
||||
|
} |
||||
|
List<Long> subtree = sysDeptService.getChildDeptIds(deptId); |
||||
|
Set<Long> ids = new HashSet<>(); |
||||
|
// 主部门命中子树
|
||||
|
authUserMapper.selectList(new LambdaQueryWrapper<AuthUser>() |
||||
|
.select(AuthUser::getId).in(AuthUser::getDeptId, subtree)) |
||||
|
.forEach(u -> ids.add(u.getId())); |
||||
|
// 兼职命中子树
|
||||
|
sysUserDeptMapper.selectList(new LambdaQueryWrapper<SysUserDept>() |
||||
|
.select(SysUserDept::getUserId).in(SysUserDept::getDeptId, subtree)) |
||||
|
.forEach(ud -> ids.add(ud.getUserId())); |
||||
|
return new ArrayList<>(ids); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构造空的 UserListDTO 分页结果(部门子树命中空集 / 查询结果为空时短路返回)。 |
||||
|
* 分页信息从底层 Page 继承,content 置空列表。 |
||||
|
*/ |
||||
|
private PageResult<UserListDTO> emptyUserDTOPage(IPage<?> page) { |
||||
|
PageResult<UserListDTO> r = new PageResult<>(); |
||||
|
r.setContent(Collections.emptyList()); |
||||
|
r.setTotal(page.getTotal()); |
||||
|
r.setSize(page.getSize()); |
||||
|
r.setCurrent(page.getCurrent()); |
||||
|
r.setPages(page.getPages()); |
||||
|
r.setEmpty(page.getTotal() == 0); |
||||
|
return r; |
||||
|
} |
||||
|
|
||||
|
private UserListDTO assembleUserListDTO(AuthUser u, |
||||
|
Map<Long, List<Long>> partTimeByUser, |
||||
|
Map<Long, List<Long>> roleIdsByUser, |
||||
|
Map<Long, String> deptNameMap, |
||||
|
Map<Long, String> roleNameMap) { |
||||
|
UserListDTO dto = new UserListDTO(); |
||||
|
BeanUtil.copyProperties(u, dto); |
||||
|
dto.setPrimaryDeptId(u.getDeptId()); |
||||
|
dto.setPrimaryDeptName(u.getDeptId() != null ? deptNameMap.get(u.getDeptId()) : null); |
||||
|
|
||||
|
List<Long> ptIds = partTimeByUser.getOrDefault(u.getId(), Collections.emptyList()); |
||||
|
dto.setPartTimeDepts(ptIds.stream() |
||||
|
.map(id -> new UserListDTO.DeptRef(id, deptNameMap.get(id))) |
||||
|
.toList()); |
||||
|
List<Long> rIds = roleIdsByUser.getOrDefault(u.getId(), Collections.emptyList()); |
||||
|
dto.setRoles(rIds.stream() |
||||
|
.map(id -> new UserListDTO.RoleRef(id, roleNameMap.get(id))) |
||||
|
.toList()); |
||||
|
return dto; |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@ |
|||||
------------------------------------------------------------------------------- |
------------------------------------------------------------------------------- |
||||
Test set: com.crm.auth.service.impl.AuthUserServiceImplTest |
Test set: com.crm.auth.service.impl.AuthUserServiceImplTest |
||||
------------------------------------------------------------------------------- |
------------------------------------------------------------------------------- |
||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.168 s -- in com.crm.auth.service.impl.AuthUserServiceImplTest |
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.181 s -- in com.crm.auth.service.impl.AuthUserServiceImplTest |
||||
|
|||||
@ -1,4 +1,4 @@ |
|||||
------------------------------------------------------------------------------- |
------------------------------------------------------------------------------- |
||||
Test set: com.crm.auth.service.impl.UserListIntegrationTest |
Test set: com.crm.auth.service.impl.UserListIntegrationTest |
||||
------------------------------------------------------------------------------- |
------------------------------------------------------------------------------- |
||||
Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.338 s -- in com.crm.auth.service.impl.UserListIntegrationTest |
Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.132 s -- in com.crm.auth.service.impl.UserListIntegrationTest |
||||
|
|||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue