package com.jtzx.crm.module.sys.service; import cc.uncarbon.framework.core.context.TenantContext; import cc.uncarbon.framework.core.context.TenantContextHolder; import cc.uncarbon.framework.core.context.UserContextHolder; import cc.uncarbon.framework.core.enums.EnabledStatusEnum; import cc.uncarbon.framework.core.exception.BusinessException; import cc.uncarbon.framework.core.page.PageParam; import cc.uncarbon.framework.core.page.PageResult; import cc.uncarbon.framework.core.props.HelioProperties; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.jtzx.crm.module.sys.constant.SysConstant; import com.jtzx.crm.module.sys.entity.SysTenantEntity; import com.jtzx.crm.module.sys.entity.SysUserEntity; import com.jtzx.crm.module.sys.enums.SysErrorEnum; import com.jtzx.crm.module.sys.enums.SysUserStatusEnum; import com.jtzx.crm.module.sys.mapper.SysUserMapper; import com.jtzx.crm.module.sys.model.interior.UserDeptContainer; import com.jtzx.crm.module.sys.model.interior.UserRoleContainer; import com.jtzx.crm.module.sys.model.request.*; import com.jtzx.crm.module.sys.model.response.SysUserBO; import com.jtzx.crm.module.sys.model.response.SysUserLoginBO; import com.jtzx.crm.module.sys.model.response.VbenAdminUserInfoVO; import com.jtzx.crm.module.sys.util.PwdUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigInteger; import java.time.LocalDateTime; import java.util.*; /** * 后台用户 */ @RequiredArgsConstructor @Service @Slf4j public class SysUserService { private final SysUserMapper sysUserMapper; private final SysRoleService sysRoleService; private final SysDeptService sysDeptService; private final SysMenuService sysMenuService; private final SysTenantService sysTenantService; private final SysUserDeptRelationService sysUserDeptRelationService; private final SysUserRoleRelationService sysUserRoleRelationService; private final SysRoleMenuRelationService sysRoleMenuRelationService; private final HelioProperties helioProperties; /** * 后台管理-分页列表 */ public PageResult adminList(PageParam pageParam, AdminListSysUserDTO dto) { // 预处理:根据【手动选择的部门】筛选用户 Set deptUserIds = Collections.emptySet(); if (dto.needFilterBySelectedDeptId()) { deptUserIds = sysUserDeptRelationService.listUserIdsByDeptIds(Collections.singleton(dto.getSelectedDeptId())); if (CollUtil.isEmpty(deptUserIds)) { // 【手动选择的部门】没有任何用户ID,直接返回空列表 return new PageResult<>(pageParam); } } // 预处理:根据【只能看到本部门及下级部门原则】筛选用户 Set visibleUserIds = determineVisibleDeptUserIds(); if (Objects.equals(CollUtil.getFirst(visibleUserIds), BigInteger.ZERO.longValue())) { // 其实啥也看不到…… return new PageResult<>(pageParam); } Set invisibleUserIds = determineInvisibleUserIds(); Page entityPage = sysUserMapper.selectPage( new Page<>(pageParam.getPageNum(), pageParam.getPageSize()), new QueryWrapper() .lambda() // 手机号 .like(CharSequenceUtil.isNotBlank(dto.getPhoneNo()), SysUserEntity::getPhoneNo, CharSequenceUtil.cleanBlank(dto.getPhoneNo())) // 根据【手动选择的部门ID】筛选用户 .in(CollUtil.isNotEmpty(deptUserIds), SysUserEntity::getId, deptUserIds) // 根据【只能看到本部门及下级部门原则】筛选用户 .in(CollUtil.isNotEmpty(visibleUserIds), SysUserEntity::getId, visibleUserIds) // 不显示特定用户 .notIn(CollUtil.isNotEmpty(invisibleUserIds), SysUserEntity::getId, invisibleUserIds) // 排序 .orderByDesc(SysUserEntity::getCreatedAt) ); return this.entityPage2BOPage(entityPage); } /** * 根据 ID 取详情 * * @param id 主键ID * @return null or BO */ public SysUserBO getOneById(Long id) { return this.getOneById(id, false); } /** * 根据 ID 取详情 * * @param id 主键ID * @param throwIfInvalidId 是否在 ID 无效时抛出异常 * @return null or BO */ public SysUserBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException { dataScopeCheck(Collections.singleton(id)); SysUserEntity entity = sysUserMapper.selectById(id); if (throwIfInvalidId) { SysErrorEnum.INVALID_ID.assertNotNull(entity); } return this.entity2BO(entity, true); } /** * 后台管理-新增 * * @return 主键ID */ @Transactional(rollbackFor = Exception.class) public Long adminInsert(AdminInsertOrUpdateSysUserDTO dto) { log.info("[后台管理-新增后台用户] >> 入参={}", dto); this.checkExistence(dto); if (Objects.nonNull(dto.getDeptId())) { // 对传入的部门ID,做数据越权检查 UserDeptContainer deptContainer = sysDeptService.getCurrentUserDeptContainer(true); if (deptContainer.hasVisibleDepts() && !CollUtil.contains(deptContainer.getVisibleDeptIds(), dto.getDeptId())) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); } } dto.setId(null); SysUserEntity entity = new SysUserEntity(); BeanUtil.copyProperties(dto, entity); String salt = IdUtil.randomUUID(); entity .setSalt(salt) .setPin(dto.getUsername()) .setPwd(PwdUtil.encrypt(dto.getPasswordOfNewUser(), salt)); sysUserMapper.insert(entity); sysUserDeptRelationService.cleanAndBind(entity.getId(), dto.getDeptId()); return entity.getId(); } /** * 后台管理-编辑 */ @Transactional(rollbackFor = Exception.class) public void adminUpdate(AdminInsertOrUpdateSysUserDTO dto) { log.info("[后台管理-编辑后台用户] >> 入参={}", dto); preUpdateCheck(dto.getId(), dto.getStatus()); this.checkExistence(dto); SysUserEntity entity = new SysUserEntity(); BeanUtil.copyProperties(dto, entity); // 手动处理异名字段 entity.setPin(dto.getUsername()); sysUserDeptRelationService.cleanAndBind(dto.getId(), dto.getDeptId()); sysUserMapper.updateById(entity); } /** * 后台管理-删除 */ @Transactional(rollbackFor = Exception.class) public void adminDelete(Collection ids) { log.info("[后台管理-删除后台用户] >> 入参={}", ids); preDeleteCheck(ids); sysUserMapper.deleteByIds(ids); } /** * 后台管理-登录 */ public SysUserLoginBO adminLogin(SysUserLoginDTO dto) { // 不要直接提示“账号不存在”或“密码不正确”,避免撞库攻击 SysUserEntity sysUserEntity = this.getUserByPin(dto.getUsername()); if (sysUserEntity == null) { return null; } if (!PwdUtil.encrypt(dto.getPassword(), sysUserEntity.getSalt()).equals(sysUserEntity.getPwd())) { return null; } if (SysUserStatusEnum.BANNED == sysUserEntity.getStatus()) { return null; } /* 以上为有效性校验, 进入实际业务逻辑 --------------------------------------------------- */ this.updateLastLoginAt(sysUserEntity.getId(), LocalDateTimeUtil.now()); // 取账号完整信息 SysUserBO sysUserBO = this.entity2BO(sysUserEntity, false); Map roleMap = sysRoleService.getRoleMapByUserId(sysUserBO.getId()); // Map> roleIdPermissionMap = sysMenuService.getRoleIdPermissionMap(roleMap.keySet()); // 包装返回体;有的字段类型不一致, 单独转换 SysUserLoginBO ret = new SysUserLoginBO(); BeanUtil.copyProperties(sysUserBO, ret); // Set permissions = roleIdPermissionMap.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); ret .setRoleIds(new HashSet<>(roleMap.keySet())) .setRoles(new ArrayList<>(roleMap.values())); // .setPermissions(permissions) // .setRoleIdPermissionMap(roleIdPermissionMap); return ret; } /** * 后台管理-取当前用户信息 */ public VbenAdminUserInfoVO adminGetCurrentUserInfo() { SysUserBO sysUserBO = this.getOneById(UserContextHolder.getUserId(), true); return VbenAdminUserInfoVO.builder() .username(sysUserBO.getUsername()) .nickname(sysUserBO.getNickname()) .lastLoginAt(sysUserBO.getLastLoginAt()) .gender(sysUserBO.getGender()) .email(sysUserBO.getEmail()) .phoneNo(sysUserBO.getPhoneNo()) .avatar(sysUserBO.getAvatarUrl()) .build(); } /** * 后台管理-重置某用户密码 */ public void adminResetUserPassword(AdminResetSysUserPasswordDTO dto) { preUpdateCheck(dto.getUserId(), null); SysUserEntity sysUserEntity = sysUserMapper.selectById(dto.getUserId()); SysUserEntity templateEntity = new SysUserEntity(); templateEntity .setPwd(PwdUtil.encrypt(dto.getRandomPassword(), sysUserEntity.getSalt())) .setId(dto.getUserId()); sysUserMapper.updateById(templateEntity); } /** * 后台管理-修改当前用户密码 */ public void adminUpdateCurrentUserPassword(AdminUpdateCurrentSysUserPasswordDTO dto) { SysUserEntity sysUserEntity = sysUserMapper.selectById(UserContextHolder.getUserId()); if (sysUserEntity == null || !sysUserEntity.getPwd().equals(PwdUtil.encrypt(dto.getOldPassword(), sysUserEntity.getSalt()))) { throw new BusinessException(SysErrorEnum.INCORRECT_OLD_PASSWORD); } sysUserEntity .setPwd(PwdUtil.encrypt(dto.getConfirmNewPassword(), sysUserEntity.getSalt())) .setId(UserContextHolder.getUserId()); sysUserMapper.updateById(sysUserEntity); } /** * 后台管理-绑定用户与角色关联关系 */ public void adminBindRoles(AdminBindUserRoleRelationDTO dto) { preBindUserRoleRelationCheck(dto); sysUserRoleRelationService.cleanAndBind(dto.getUserId(), dto.getRoleIds()); } /** * 根据用户账号查询 */ public SysUserEntity getUserByPin(String pin) { return sysUserMapper.getUserByPin(pin); } /** * 后台管理 - 取指定用户关联角色ID * * @param userId 用户ID * @return 角色Ids */ public Set listRelatedRoleIds(Long userId) { if (ObjectUtil.isNull(userId)) { return Collections.emptySet(); } return sysRoleService.getRoleMapByUserId(userId).keySet(); } /** * 后台管理 - 取租户用户IDs * * @param tenantId 租户ID,非主键ID * @param statusEnums 仅保留符合指定状态的,可以为null */ public List listUserIdsByTenantId(Long tenantId, Collection statusEnums) { if (Objects.isNull(tenantId)) { return Collections.emptyList(); } // 备份原始租户上下文;以下查询方式可同时兼容行级、数据源级多租户 TenantContext originContext = TenantContextHolder.getTenantContext(); try { // 临时切换租户 TenantContextHolder.setTenantContext(new TenantContext(tenantId, CharSequenceUtil.EMPTY)); return sysUserMapper.selectIds(statusEnums); } finally { TenantContextHolder.setTenantContext(originContext); } } /** * 后台管理-更新当前用户信息资料 */ @Transactional(rollbackFor = Exception.class) public void adminUpdateCurrentUserInfo(AdminUpdateCurrentSysUserInfoDTO dto) { SysUserEntity update = SysUserEntity.of(dto); update.setId(UserContextHolder.getUserId()); sysUserMapper.updateById(update); } /** * 后台管理-更新当前用户头像 */ @Transactional(rollbackFor = Exception.class) public void adminUpdateCurrentUserAvatar(AdminUpdateCurrentSysUserAvatarDTO dto) { dto.securityCheck(); SysUserEntity update = SysUserEntity.of(dto); update.setId(UserContextHolder.getUserId()); sysUserMapper.updateById(update); } /* ---------------------------------------------------------------- 私有方法 private methods ---------------------------------------------------------------- */ /** * 实体转 BO * * @param entity 实体 * @param fillDeptInfo 是否根据实体部门ID,查询关联部门信息并填充到BO * @return BO */ private SysUserBO entity2BO(SysUserEntity entity, boolean fillDeptInfo) { if (entity == null) { return null; } SysUserBO bo = new SysUserBO(); BeanUtil.copyProperties(entity, bo); // 可以在此处为BO填充字段 bo.setUsername(entity.getPin()); if (fillDeptInfo) { Optional.ofNullable(sysDeptService.getSpecifiedUserDeptContainer(bo.getId(), false)) .map(UserDeptContainer::primaryRelatedDept) .ifPresent(deptInfo -> bo.setDeptId(deptInfo.getId()).setDeptTitle(deptInfo.getTitle())); } return bo; } /** * 实体 List 转 BO List * * @param entityList 实体 List * @return BO List */ private List entityList2BOs(List entityList) { if (CollUtil.isEmpty(entityList)) { return Collections.emptyList(); } // 深拷贝 List ret = new ArrayList<>(entityList.size()); entityList.forEach( entity -> ret.add(this.entity2BO(entity, true)) ); return ret; } /** * 实体分页转 BO 分页 * * @param entityPage 实体分页 * @return BO 分页 */ private PageResult entityPage2BOPage(Page entityPage) { return new PageResult() .setCurrent(entityPage.getCurrent()) .setSize(entityPage.getSize()) .setTotal(entityPage.getTotal()) .setRecords(this.entityList2BOs(entityPage.getRecords())); } /** * 检查是否已存在相同数据 * * @param dto DTO */ private void checkExistence(AdminInsertOrUpdateSysUserDTO dto) { SysUserEntity existingEntity = this.getUserByPin(dto.getUsername()); if (existingEntity != null && !existingEntity.getId().equals(dto.getId())) { throw new BusinessException(400, "已存在相同账号,请重新输入"); } } /** * 确定本部门及下级部门用户IDs * 返回空集合代表不限制 * 返回[0]或有元素集合,表示有限制 */ private Set determineVisibleDeptUserIds() { Set visibleUserIds = Collections.emptySet(); UserDeptContainer deptContainer = sysDeptService.getCurrentUserDeptContainer(true); if (deptContainer.hasVisibleDepts()) { visibleUserIds = sysUserDeptRelationService.listUserIdsByDeptIds(deptContainer.getVisibleDeptIds()); if (CollUtil.isEmpty(visibleUserIds)) { // 【可见部门】没有任何用户ID,直接返回[0] return Collections.singleton(BigInteger.ZERO.longValue()); } } return visibleUserIds; } /** * 确定不可见用户IDs * 租户管理员:列表中不显示超级管理员用户 * 普通用户:列表中不显示超级管理员、租户管理员用户 */ private Set determineInvisibleUserIds() { Set invisibleRoleIds = sysRoleService.determineInvisibleRoleIds(); return sysUserRoleRelationService.listUserIdsByRoleIds(invisibleRoleIds); } /** * 数据越权检查 */ private void dataScopeCheck(Collection userIds) { Set visibleUserIds = determineVisibleDeptUserIds(); Set invisibleUserIds = determineInvisibleUserIds(); if (CollUtil.isNotEmpty(visibleUserIds) && !CollUtil.containsAll(visibleUserIds, userIds) || CollUtil.isNotEmpty(invisibleUserIds) && CollUtil.containsAny(invisibleUserIds, userIds)) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); } } /** * 检查并获取租户上下文 bean,无效或被禁用则直接抛出异常 * * @param tenantId 租户ID * @return TenantContext */ private TenantContext checkAndGetTenantContext(Long tenantId) throws BusinessException { // 查询租户是否仍有效 SysTenantEntity tenantEntity = sysTenantService.getTenantEntityByTenantId(tenantId); if (tenantEntity == null) { throw new BusinessException(SysErrorEnum.INVALID_TENANT); } if (EnabledStatusEnum.DISABLED == tenantEntity.getStatus()) { throw new BusinessException(SysErrorEnum.DISABLED_TENANT); } return TenantContext.builder() .tenantId(tenantEntity.getTenantId()) .tenantName(tenantEntity.getTenantName()) .build(); } private void updateLastLoginAt(Long userId, LocalDateTime lastLoginAt) { SysUserEntity entity = new SysUserEntity(); entity .setLastLoginAt(lastLoginAt) .setId(userId); sysUserMapper.updateById(entity); } /** * 编辑后台用户信息前检查 * * @param specifiedUserId 被操作用户ID * @param statusEnum 用户状态枚举,可以为null */ private void preUpdateCheck(Long specifiedUserId, @Nullable SysUserStatusEnum statusEnum) { UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer(); if (currentUser.isAdmin()) { // 超级管理员除禁用自己外为所欲为 if (statusEnum == SysUserStatusEnum.BANNED && Objects.equals(specifiedUserId, UserContextHolder.getUserId())) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); } return; } if (Objects.equals(specifiedUserId, UserContextHolder.getUserId())) { // 不能动自身用户 throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); } // 目标是超级管理员or租户管理员时,均不能编辑 UserRoleContainer specifiedUser = sysRoleService.getSpecifiedUserRoleContainer(specifiedUserId); if (specifiedUser.isGroupManager()) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); } // dataScopeCheck(Collections.singleton(specifiedUserId)); // 暂未实现角色层级,一律平级 } /** * 删除后台用户前检查 */ private void preDeleteCheck(Collection ids) { if (CollUtil.contains(ids, UserContextHolder.getUserId())) { // 不能动自身用户 throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); } // 目标是超级管理员时,不能删除 List specifiedUsers = ids.stream().map(sysRoleService::getSpecifiedUserRoleContainer).toList(); if (specifiedUsers.stream().anyMatch(UserRoleContainer::isAdmin)) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); } // 只有超级管理员可以删租户管理员用户 UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer(); if (!currentUser.isGroupManager()) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); } dataScopeCheck(ids); // 暂未实现角色层级,一律平级 } /** * 绑定后台用户与角色关联关系前检查 * 防止越权访问漏洞 */ private void preBindUserRoleRelationCheck(AdminBindUserRoleRelationDTO dto) { UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer(); // 是否对自己操作 boolean selfFlag = Objects.equals(dto.getUserId(), UserContextHolder.getUserId()); if (currentUser.isAdmin()) { // 超级管理员不能去掉自己的超级管理员角色 if (selfFlag && !CollUtil.contains(dto.getRoleIds(), SysConstant.SUPER_ADMIN_ROLE_ID)) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); } // 也不能赋予其他人超级管理员角色 if (!selfFlag && CollUtil.contains(dto.getRoleIds(), SysConstant.SUPER_ADMIN_ROLE_ID)) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); } return; } if (selfFlag) { // 不能动自身用户 throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); } // 目标已经是超级管理员or租户管理员时,均不能绑定 UserRoleContainer specifiedUser = sysRoleService.getSpecifiedUserRoleContainer(dto.getUserId()); if (specifiedUser.isAdmin()) { throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); } // 超级管理员之外的用户,都需要校验自身角色范围是否满足输入值 currentUserNotSuperAdmin(dto, currentUser); dataScopeCheck(Collections.singleton(dto.getUserId())); } /** * 绑定后台用户与角色关联关系前检查 * 超级管理员之外的用户,都需要校验自身角色范围是否满足输入值 * 拆分子方法以降低Cognitive Complexity */ private void currentUserNotSuperAdmin(AdminBindUserRoleRelationDTO dto, UserRoleContainer currentUser) { if (CollUtil.isNotEmpty(dto.getRoleIds()) && !currentUser.isAdmin()) { boolean overRoles = !CollUtil.containsAll(currentUser.getRelatedRoleIds(), dto.getRoleIds()); if (overRoles && currentUser.isGroupManager()) { // 普通用户超自身角色授予了;如果当前用户拥有新角色的所有菜单,那么也放行 Set grantedMenuIds = sysRoleMenuRelationService.listMenuIdsByRoleIds(currentUser.getRelatedRoleIds()); Set needMenuIds = sysRoleMenuRelationService.listMenuIdsByRoleIds(dto.getRoleIds()); if (!CollUtil.containsAll(grantedMenuIds, needMenuIds)) { throw new BusinessException(SysErrorEnum.BEYOND_AUTHORITY_BIND_ROLES); } } // if (currentUser.isTenantAdmin()) { // // 超自身权限,但作为租户管理员有额外情况 // Set invisibleRoleIds = sysRoleService.determineInvisibleRoleIds(); // // 除非超越了可见角色IDs授予 or 想要授予用户租户管理员角色,否则不管 // invisibleRoleIds.addAll(currentUser.getRelatedRoleIds()); // if (CollUtil.containsAny(invisibleRoleIds, dto.getRoleIds())) { // throw new BusinessException(SysErrorEnum.BEYOND_AUTHORITY_BIND_ROLES); // } // } } } }