package cn.iocoder.yudao.module.system.service.user; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils; import cn.iocoder.yudao.module.infra.api.config.ConfigApi; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthRegisterReqVO; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserImportExcelVO; import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserImportRespVO; import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.dal.mysql.dept.UserPostMapper; import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper; import cn.iocoder.yudao.module.system.mq.producer.user.AdminUserProducer; import cn.iocoder.yudao.module.system.service.dept.DeptService; import cn.iocoder.yudao.module.system.service.dept.PostService; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; import cn.iocoder.yudao.module.system.service.permission.PermissionService; import cn.iocoder.yudao.module.system.service.tenant.TenantService; import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils; import com.google.common.annotations.VisibleForTesting; import com.mzt.logapi.context.LogRecordContext; import com.mzt.logapi.service.impl.DiffParseFunction; import com.mzt.logapi.starter.annotation.LogRecord; import jakarta.annotation.Resource; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.system.enums.LogRecordConstants.*; /** * 后台用户 Service 实现类 * * @author 超级管理员 */ @Service("adminUserService") @Slf4j public class AdminUserServiceImpl implements AdminUserService { static final String USER_INIT_PASSWORD_KEY = "system.user.init-password"; static final String USER_REGISTER_ENABLED_KEY = "system.user.register-enabled"; @Resource private AdminUserMapper userMapper; @Resource private DeptService deptService; @Resource private PostService postService; @Resource private PermissionService permissionService; @Resource private PasswordEncoder passwordEncoder; @Resource @Lazy // 延迟,避免循环依赖报错 private TenantService tenantService; @Resource @Lazy // 懒加载,避免循环依赖 private OAuth2TokenService oauth2TokenService; @Resource private UserPostMapper userPostMapper; @Resource private ConfigApi configApi; @Resource private AdminUserProducer adminUserProducer; @Override @Transactional(rollbackFor = Exception.class) @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_CREATE_SUB_TYPE, bizNo = "{{#user.id}}", success = SYSTEM_USER_CREATE_SUCCESS) public Long createUser(UserSaveReqVO createReqVO) { // 1.1 校验账户配合 tenantService.handleTenantInfo(tenant -> { long count = userMapper.selectCount(); if (count >= tenant.getAccountCount()) { throw exception(USER_COUNT_MAX, tenant.getAccountCount()); } }); // 1.2 校验正确性 validateUserForCreateOrUpdate(null, createReqVO.getUsername(), createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds()); // 2.1 插入用户 AdminUserDO user = BeanUtils.toBean(createReqVO, AdminUserDO.class); user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 user.setPassword(encodePassword(createReqVO.getPassword())); // 加密密码 userMapper.insert(user); // 2.2 插入关联岗位 if (CollectionUtil.isNotEmpty(user.getPostIds())) { userPostMapper.insertBatch(convertList(user.getPostIds(), postId -> new UserPostDO().setUserId(user.getId()).setPostId(postId))); } // 2.3 分配用户角色 if (CollUtil.isNotEmpty(createReqVO.getRoleIds())) { permissionService.assignUserRole(user.getId(), createReqVO.getRoleIds()); } // 3. 记录操作日志上下文 LogRecordContext.putVariable("user", user); return user.getId(); } @Override public Long registerUser(AuthRegisterReqVO registerReqVO) { // 1.1 校验是否开启注册 if (ObjUtil.notEqual(configApi.getConfigValueByKey(USER_REGISTER_ENABLED_KEY), "true")) { throw exception(USER_REGISTER_DISABLED); } // 1.2 校验账户配合 tenantService.handleTenantInfo(tenant -> { long count = userMapper.selectCount(); if (count >= tenant.getAccountCount()) { throw exception(USER_COUNT_MAX, tenant.getAccountCount()); } }); // 1.3 校验正确性 validateUserForCreateOrUpdate(null, registerReqVO.getUsername(), null, null, null, null); // 2. 插入用户 AdminUserDO user = BeanUtils.toBean(registerReqVO, AdminUserDO.class); user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 user.setPassword(encodePassword(registerReqVO.getPassword())); // 加密密码 userMapper.insert(user); return user.getId(); } @Override @Transactional(rollbackFor = Exception.class) @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = SYSTEM_USER_UPDATE_SUCCESS) public void updateUser(UserSaveReqVO updateReqVO) { updateReqVO.setPassword(null); // 特殊:此处不更新密码 // 1. 校验正确性 AdminUserDO oldUser = validateUserForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getUsername(), updateReqVO.getMobile(), updateReqVO.getEmail(), updateReqVO.getDeptId(), updateReqVO.getPostIds()); // 2.1 更新用户 AdminUserDO updateObj = BeanUtils.toBean(updateReqVO, AdminUserDO.class); userMapper.updateById(updateObj); // 2.2 更新岗位 updateUserPost(updateReqVO, updateObj); // 2.3 昵称 / 头像变化时,发送消息供下游订阅(如 IM 模块推 FRIEND_INFO_UPDATED) publishUserProfileUpdatedIfChanged(oldUser, updateReqVO.getNickname(), updateReqVO.getAvatar()); // 3. 记录操作日志上下文 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldUser, UserSaveReqVO.class)); LogRecordContext.putVariable("user", oldUser); } private void updateUserPost(UserSaveReqVO reqVO, AdminUserDO updateObj) { Long userId = reqVO.getId(); Set dbPostIds = convertSet(userPostMapper.selectListByUserId(userId), UserPostDO::getPostId); // 计算新增和删除的岗位编号 Set postIds = CollUtil.emptyIfNull(updateObj.getPostIds()); Collection createPostIds = CollUtil.subtract(postIds, dbPostIds); Collection deletePostIds = CollUtil.subtract(dbPostIds, postIds); // 执行新增和删除。对于已经授权的岗位,不用做任何处理 if (!CollectionUtil.isEmpty(createPostIds)) { userPostMapper.insertBatch(convertList(createPostIds, postId -> new UserPostDO().setUserId(userId).setPostId(postId))); } if (!CollectionUtil.isEmpty(deletePostIds)) { userPostMapper.deleteByUserIdAndPostId(userId, deletePostIds); } } @Override public void updateUserLogin(Long id, String loginIp) { userMapper.updateById(new AdminUserDO().setId(id).setLoginIp(loginIp).setLoginDate(LocalDateTime.now())); } @Override public void updateUserProfile(Long id, UserProfileUpdateReqVO reqVO) { // 1. 校验正确性 AdminUserDO oldUser = validateUserExists(id); validateEmailUnique(id, reqVO.getEmail()); validateMobileUnique(id, reqVO.getMobile()); // 2. 执行更新 userMapper.updateById(BeanUtils.toBean(reqVO, AdminUserDO.class).setId(id)); // 3. 昵称 / 头像变化时,发送消息供下游订阅(如 IM 模块推 FRIEND_INFO_UPDATED) publishUserProfileUpdatedIfChanged(oldUser, reqVO.getNickname(), reqVO.getAvatar()); } /** * 仅当 nickname 或 avatar 跟旧值不一致时,发送 AdminUserProfileUpdateMessage */ private void publishUserProfileUpdatedIfChanged(AdminUserDO oldUser, String newNickname, String newAvatar) { boolean nicknameChanged = newNickname != null && !ObjUtil.equal(oldUser.getNickname(), newNickname); boolean avatarChanged = newAvatar != null && !ObjUtil.equal(oldUser.getAvatar(), newAvatar); if (!nicknameChanged && !avatarChanged) { return; } adminUserProducer.sendUserProfileUpdateMessage(oldUser.getId(), nicknameChanged ? newNickname : null, avatarChanged ? newAvatar : null); } @Override public void updateUserPassword(Long id, UserProfileUpdatePasswordReqVO reqVO) { // 校验旧密码密码 validateOldPassword(id, reqVO.getOldPassword()); // 执行更新 AdminUserDO updateObj = new AdminUserDO().setId(id); updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码 userMapper.updateById(updateObj); } @Override @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE, bizNo = "{{#id}}", success = SYSTEM_USER_UPDATE_PASSWORD_SUCCESS) public void updateUserPassword(Long id, String password) { // 1. 校验用户存在 AdminUserDO user = validateUserExists(id); // 2. 更新密码 AdminUserDO updateObj = new AdminUserDO(); updateObj.setId(id); updateObj.setPassword(encodePassword(password)); // 加密密码 userMapper.updateById(updateObj); // 3. 记录操作日志上下文 LogRecordContext.putVariable("user", user); LogRecordContext.putVariable("newPassword", updateObj.getPassword()); } @Override public void updateUserStatus(Long id, Integer status) { // 校验用户存在 validateUserExists(id); // 更新状态 AdminUserDO updateObj = new AdminUserDO(); updateObj.setId(id); updateObj.setStatus(status); userMapper.updateById(updateObj); // 如果是禁用用户,则删除其 Token 信息 if (CommonStatusEnum.isDisable(status)) { oauth2TokenService.removeAccessToken(id, UserTypeEnum.ADMIN.getValue()); } } @Override @Transactional(rollbackFor = Exception.class) @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_DELETE_SUB_TYPE, bizNo = "{{#id}}", success = SYSTEM_USER_DELETE_SUCCESS) public void deleteUser(Long id) { // 1. 校验用户存在 AdminUserDO user = validateUserExists(id); // 2.1 删除用户 userMapper.deleteById(id); // 2.2 删除用户关联数据 permissionService.processUserDeleted(id); // 2.2 删除用户岗位 userPostMapper.deleteByUserId(id); // 3. 记录操作日志上下文 LogRecordContext.putVariable("user", user); } @Override @Transactional(rollbackFor = Exception.class) public void deleteUserList(List ids) { // 1. 批量删除用户 userMapper.deleteByIds(ids); // 2. 批量删除用户关联数据 ids.forEach(id -> { permissionService.processUserDeleted(id); userPostMapper.deleteByUserId(id); }); } @Override public AdminUserDO getUserByUsername(String username) { return userMapper.selectByUsername(username); } @Override public AdminUserDO getUserByMobile(String mobile) { return userMapper.selectByMobile(mobile); } @Override public PageResult getUserPage(UserPageReqVO reqVO) { // 如果有角色编号,查询角色对应的用户编号 Set userIds = null; if (reqVO.getRoleId() != null) { userIds = permissionService.getUserRoleIdListByRoleId(singleton(reqVO.getRoleId())); if (CollUtil.isEmpty(userIds)) { return PageResult.empty(); } } // 分页查询 return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()), userIds); } @Override public AdminUserDO getUser(Long id) { return userMapper.selectById(id); } @Override public List getUserListByDeptIds(Collection deptIds) { if (CollUtil.isEmpty(deptIds)) { return Collections.emptyList(); } return userMapper.selectListByDeptIds(deptIds); } @Override public List getUserListByPostIds(Collection postIds) { if (CollUtil.isEmpty(postIds)) { return Collections.emptyList(); } Set userIds = convertSet(userPostMapper.selectListByPostIds(postIds), UserPostDO::getUserId); if (CollUtil.isEmpty(userIds)) { return Collections.emptyList(); } return userMapper.selectByIds(userIds); } @Override public List getUserList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return userMapper.selectByIds(ids); } public List getUserListAll() { return userMapper.selectList(); } @Override public void validateUserList(Collection ids) { if (CollUtil.isEmpty(ids)) { return; } // 获得岗位信息 List users = userMapper.selectByIds(ids); Map userMap = CollectionUtils.convertMap(users, AdminUserDO::getId); // 校验 ids.forEach(id -> { AdminUserDO user = userMap.get(id); if (user == null) { throw exception(USER_NOT_EXISTS); } if (!CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus())) { throw exception(USER_IS_DISABLE, user.getNickname()); } }); } @Override public List getUserListByNickname(String nickname) { return userMapper.selectListByNickname(nickname); } /** * 获得部门条件:查询指定部门的子部门编号们,包括自身 * * @param deptId 部门编号 * @return 部门编号集合 */ private Set getDeptCondition(Long deptId) { if (deptId == null) { return Collections.emptySet(); } Set deptIds = convertSet(deptService.getChildDeptList(deptId), DeptDO::getId); deptIds.add(deptId); // 包括自身 return deptIds; } private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email, Long deptId, Set postIds) { // 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确 return DataPermissionUtils.executeIgnore(() -> { // 校验用户存在 AdminUserDO user = validateUserExists(id); // 校验用户名唯一 validateUsernameUnique(id, username); // 校验手机号唯一 validateMobileUnique(id, mobile); // 校验邮箱唯一 validateEmailUnique(id, email); // 校验部门处于开启状态 deptService.validateDeptList(CollectionUtils.singleton(deptId)); // 校验岗位处于开启状态 postService.validatePostList(postIds); return user; }); } @VisibleForTesting AdminUserDO validateUserExists(Long id) { if (id == null) { return null; } AdminUserDO user = userMapper.selectById(id); if (user == null) { throw exception(USER_NOT_EXISTS); } return user; } @VisibleForTesting void validateUsernameUnique(Long id, String username) { if (StrUtil.isBlank(username)) { return; } AdminUserDO user = userMapper.selectByUsername(username); if (user == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的用户 if (id == null) { throw exception(USER_USERNAME_EXISTS); } if (!user.getId().equals(id)) { throw exception(USER_USERNAME_EXISTS); } } @VisibleForTesting void validateEmailUnique(Long id, String email) { if (StrUtil.isBlank(email)) { return; } AdminUserDO user = userMapper.selectByEmail(email); if (user == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的用户 if (id == null) { throw exception(USER_EMAIL_EXISTS); } if (!user.getId().equals(id)) { throw exception(USER_EMAIL_EXISTS); } } @VisibleForTesting void validateMobileUnique(Long id, String mobile) { if (StrUtil.isBlank(mobile)) { return; } AdminUserDO user = userMapper.selectByMobile(mobile); if (user == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的用户 if (id == null) { throw exception(USER_MOBILE_EXISTS); } if (!user.getId().equals(id)) { throw exception(USER_MOBILE_EXISTS); } } /** * 校验旧密码 * @param id 用户 id * @param oldPassword 旧密码 */ @VisibleForTesting void validateOldPassword(Long id, String oldPassword) { AdminUserDO user = userMapper.selectById(id); if (user == null) { throw exception(USER_NOT_EXISTS); } if (!isPasswordMatch(oldPassword, user.getPassword())) { throw exception(USER_PASSWORD_FAILED); } } @Override @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_IMPORT_SUB_TYPE, bizNo = "0", success = SYSTEM_USER_IMPORT_SUCCESS) @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 public UserImportRespVO importUserList(List importUsers, boolean isUpdateSupport) { // 1.1 参数校验 if (CollUtil.isEmpty(importUsers)) { throw exception(USER_IMPORT_LIST_IS_EMPTY); } // 1.2 初始化密码:Excel 的「初始密码」列留空时,使用系统配置的初始密码 String initPassword = configApi.getConfigValueByKey(USER_INIT_PASSWORD_KEY); // 1.3 预加载部门、岗位,用于把 Excel 里填写的「名称」翻译成编号 List deptList = DataPermissionUtils.executeIgnore(() -> deptService.getDeptList(new DeptListReqVO())); List postList = postService.getPostList(null, null); // 2. 遍历,逐个创建 or 更新 UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>()) .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build(); AtomicInteger index = new AtomicInteger(1); importUsers.forEach(importUser -> { int currentIndex = index.getAndIncrement(); String username = StrUtil.trim(importUser.getUsername()); // 失败明细的 key 定位到具体的行(并带上用户账号),方便用户在 Excel 中找到这一行 String rowKey = StrUtil.format("第 {} 行{}", currentIndex, StrUtil.isEmpty(username) ? "" : StrUtil.format("「{}」", username)); try { // 2.1.1 解析部门、岗位(支持填名称,部门兼容填编号) Long deptId = parseDeptId(importUser.getDept(), deptList); Set postIds = parsePostIds(importUser.getPost(), postList); // 2.1.2 校验字段是否符合要求。密码优先取 Excel 的「初始密码」,为空时取系统配置 String password = StrUtil.blankToDefault(StrUtil.trim(importUser.getPassword()), initPassword); UserSaveReqVO saveReqVO = BeanUtils.toBean(importUser, UserSaveReqVO.class) .setUsername(username).setDeptId(deptId).setPostIds(postIds).setPassword(password); validateUserImportRow(saveReqVO, password); // 2.1.3 校验用户账号、手机号、邮箱唯一,以及部门、岗位可用 validateUserForCreateOrUpdate(null, username, saveReqVO.getMobile(), saveReqVO.getEmail(), deptId, postIds); // 2.2.1 判断如果不存在,在进行插入 AdminUserDO existUser = userMapper.selectByUsername(username); if (existUser == null) { AdminUserDO user = BeanUtils.toBean(saveReqVO, AdminUserDO.class) .setPassword(encodePassword(password)) .setStatus(ObjUtil.defaultIfNull(importUser.getStatus(), CommonStatusEnum.ENABLE.getStatus())); //「账号状态」留空时默认开启 userMapper.insert(user); insertUserPost(user.getId(), postIds); // 同步岗位关联,避免导入出来的用户没有岗位 respVO.getCreateUsernames().add(username); return; } // 2.2.2 如果存在,判断是否允许更新 if (!isUpdateSupport) { throw new ServiceException(USER_USERNAME_EXISTS.getCode(), "用户账号已存在。如需覆盖这个用户,请勾选「是否覆盖」后重新导入;否则请修改 Excel 中的用户账号"); } AdminUserDO updateUser = BeanUtils.toBean(saveReqVO, AdminUserDO.class) .setId(existUser.getId()).setPassword(null) // 覆盖时不改密码,避免把在用账号的密码改掉 .setStatus(importUser.getStatus()); //「账号状态」留空时保持原状态 userMapper.updateById(updateUser); userPostMapper.deleteByUserId(existUser.getId()); insertUserPost(existUser.getId(), postIds); respVO.getUpdateUsernames().add(username); } catch (ConstraintViolationException ex) { respVO.getFailureUsernames().put(rowKey, buildConstraintFailureMessage(ex)); } catch (ServiceException ex) { respVO.getFailureUsernames().put(rowKey, buildImportFailureMessage(ex, importUser)); } }); // 记录导入结果汇总,供 @LogRecord 的 success 文案引用 OperateLogUtils.putImportSummary(respVO.getCreateUsernames(), respVO.getUpdateUsernames(), respVO.getFailureUsernames()); return respVO; } /** Bean 校验失败时的字段名 -> 给用户的修改建议 */ private static final Map USER_IMPORT_FIELD_HINTS = Map.of( "username", "「用户账号」必须填写,且只能由 4-30 位数字、字母组成(例如 zhangsan)", "nickname", "「用户昵称」最多 30 个字符", "password", "「初始密码」长度为 4-16 位", "email", "「用户邮箱」格式不正确,请填写形如 zhangsan@example.com 的邮箱,或留空该列", "mobile", "「手机号码」格式不正确,请填写 11 位手机号(例如 13800000000),或留空该列"); /** * 校验单行的账号、昵称、密码等字段是否符合要求 * * @param saveReqVO Excel 行转换出的用户信息 * @param password 该行最终使用的密码 */ private void validateUserImportRow(UserSaveReqVO saveReqVO, String password) { if (StrUtil.isEmpty(password)) { throw new ServiceException(USER_IMPORT_INIT_PASSWORD.getCode(), "未填写「初始密码」,且系统没有配置默认的初始密码。请在 Excel 的「初始密码」列填写 4-16 位密码," + "或让管理员在「基础设施 → 配置管理」中配置 system.user.init-password"); } ValidationUtils.validate(saveReqVO); } /** 把 Bean 校验结果整理成「哪个字段、怎么改」 */ private static String buildConstraintFailureMessage(ConstraintViolationException ex) { List messages = new ArrayList<>(); ex.getConstraintViolations().forEach(violation -> { // 校验的是 Bean 本身,属性路径就是字段名(如 username),兼容带前缀的写法 String path = violation.getPropertyPath().toString(); String field = StrUtil.contains(path, '.') ? StrUtil.subAfter(path, '.', true) : path; messages.add(StrUtil.blankToDefault(USER_IMPORT_FIELD_HINTS.get(field), violation.getMessage())); }); return String.join(";", messages); } /** 把系统内置的校验异常,补充成用户看得懂、知道怎么改的提示 */ private static String buildImportFailureMessage(ServiceException ex, UserImportExcelVO importUser) { if (ObjUtil.equal(ex.getCode(), USER_USERNAME_EXISTS.getCode())) { return "用户账号已存在,如需覆盖请勾选「是否覆盖」后重新导入,否则请修改 Excel 中的用户账号"; } if (ObjUtil.equal(ex.getCode(), USER_MOBILE_EXISTS.getCode())) { return StrUtil.format("手机号码「{}」已被其他用户使用,请更换手机号,或留空「手机号码」列", importUser.getMobile()); } if (ObjUtil.equal(ex.getCode(), USER_EMAIL_EXISTS.getCode())) { return StrUtil.format("邮箱「{}」已被其他用户使用,请更换邮箱,或留空「用户邮箱」列", importUser.getEmail()); } return ex.getMessage(); } /** * 解析 Excel「部门」列:支持填写部门名称,也兼容填写部门编号 * * @param deptText Excel 中填写的内容,允许为空(表示暂不归属部门) * @return 部门编号 */ private Long parseDeptId(String deptText, List deptList) { if (StrUtil.isEmpty(deptText)) { return null; } String text = StrUtil.trim(deptText); // 情况一:填写的是部门编号 if (StrUtil.isNumeric(text) && text.length() <= 18) { long deptId = Long.parseLong(text); DeptDO dept = deptList.stream().filter(item -> ObjUtil.equal(item.getId(), deptId)) .findFirst().orElse(null); if (dept == null) { throw new ServiceException(USER_IMPORT_DEPT_NOT_EXISTS.getCode(), StrUtil.format( "「部门」列填写的部门编号「{}」不存在,请在「系统管理 → 部门」中查看正确的部门编号,或直接填写部门名称", text)); } validateMasterDataEnabled(dept.getStatus(), StrUtil.format("部门「{}」已停用", dept.getName()), "请先在「系统管理 → 部门」中启用该部门"); return deptId; } // 情况二:填写的是部门名称 List matched = deptList.stream() .filter(item -> StrUtil.equals(text, StrUtil.trim(item.getName()))).toList(); if (matched.isEmpty()) { throw new ServiceException(USER_IMPORT_DEPT_NOT_EXISTS.getCode(), StrUtil.format( "「部门」列填写的部门「{}」不存在,请先在「系统管理 → 部门」中创建,或改成已存在的部门名称/编号", text)); } if (matched.size() > 1) { throw new ServiceException(USER_IMPORT_DEPT_NAME_DUPLICATE.getCode(), StrUtil.format( "部门名称「{}」在系统里有 {} 个同名部门,请改填「部门编号」(可在「系统管理 → 部门」中查看)", text, matched.size())); } DeptDO dept = matched.get(0); validateMasterDataEnabled(dept.getStatus(), StrUtil.format("部门「{}」已停用", dept.getName()), "请先在「系统管理 → 部门」中启用该部门"); return dept.getId(); } /** * 解析 Excel「岗位」列:填写岗位名称,多个岗位用「、」「,」「;」分隔 * * @return 岗位编号集合,至少一个 */ private Set parsePostIds(String postText, List postList) { if (StrUtil.isEmpty(postText)) { throw new ServiceException(USER_IMPORT_POST_EMPTY.getCode(), "未填写「岗位」,请填写岗位名称;多个岗位用「、」分隔,例如:普工、班组长"); } Set postIds = new HashSet<>(); List notExistsNames = new ArrayList<>(); for (String part : postText.split("[、,,;;/\\s]+")) { String name = StrUtil.trim(part); if (StrUtil.isEmpty(name)) { continue; } PostDO post = postList.stream().filter(item -> StrUtil.equals(name, StrUtil.trim(item.getName()))) .findFirst().orElse(null); if (post == null) { notExistsNames.add(name); continue; } validateMasterDataEnabled(post.getStatus(), StrUtil.format("岗位「{}」已停用", post.getName()), "请先在「系统管理 → 岗位」中启用该岗位"); postIds.add(post.getId()); } if (!notExistsNames.isEmpty()) { throw new ServiceException(USER_IMPORT_POST_NOT_EXISTS.getCode(), StrUtil.format( "「岗位」列填写的岗位「{}」不存在,请先在「系统管理 → 岗位」中维护,或改成已存在的岗位名称", String.join("、", notExistsNames))); } if (postIds.isEmpty()) { // 例如只填了分隔符 throw new ServiceException(USER_IMPORT_POST_EMPTY.getCode(), "未填写「岗位」,请填写岗位名称;多个岗位用「、」分隔,例如:普工、班组长"); } return postIds; } /** 校验部门、岗位是否处于开启状态 */ private static void validateMasterDataEnabled(Integer status, String disabledTip, String actionTip) { if (ObjUtil.equal(status, CommonStatusEnum.DISABLE.getStatus())) { throw new ServiceException(USER_IMPORT_MASTER_DATA_DISABLED.getCode(), StrUtil.format("{},{}", disabledTip, actionTip)); } } /** 写入用户与岗位的关联,导入出来的用户同样要有岗位(否则在页面上编辑保存不了) */ private void insertUserPost(Long userId, Set postIds) { if (CollUtil.isEmpty(postIds)) { return; } userPostMapper.insertBatch(convertList(postIds, postId -> new UserPostDO().setUserId(userId).setPostId(postId))); } @Override public List getUserListByStatus(Integer status) { return userMapper.selectListByStatus(status); } @Override public List getDeptUsers(Collection deptIds) { return userMapper.selectListByDeptIds(deptIds); } @Override public boolean isPasswordMatch(String rawPassword, String encodedPassword) { return passwordEncoder.matches(rawPassword, encodedPassword); } /** * 对密码进行加密 * * @param password 密码 * @return 加密后的密码 */ private String encodePassword(String password) { return passwordEncoder.encode(password); } }