| | |
| | | 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.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; |
| | |
| | | if (CollUtil.isEmpty(importUsers)) { |
| | | throw exception(USER_IMPORT_LIST_IS_EMPTY); |
| | | } |
| | | // 1.2 初始化密码不能为空 |
| | | // 1.2 初始化密码:Excel 的「初始密码」列留空时,使用系统配置的初始密码 |
| | | String initPassword = configApi.getConfigValueByKey(USER_INIT_PASSWORD_KEY); |
| | | if (StrUtil.isEmpty(initPassword)) { |
| | | throw exception(USER_IMPORT_INIT_PASSWORD); |
| | | } |
| | | // 1.3 预加载部门、岗位,用于把 Excel 里填写的「名称」翻译成编号 |
| | | List<DeptDO> deptList = DataPermissionUtils.executeIgnore(() -> deptService.getDeptList(new DeptListReqVO())); |
| | | List<PostDO> postList = postService.getPostList(null, null); |
| | | |
| | | // 2. 遍历,逐个创建 or 更新 |
| | | UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>()) |
| | |
| | | AtomicInteger index = new AtomicInteger(1); |
| | | importUsers.forEach(importUser -> { |
| | | int currentIndex = index.getAndIncrement(); |
| | | // 2.1.1 校验字段是否符合要求 |
| | | String username = StrUtil.trim(importUser.getUsername()); |
| | | // 失败明细的 key 定位到具体的行(并带上用户账号),方便用户在 Excel 中找到这一行 |
| | | String rowKey = StrUtil.format("第 {} 行{}", currentIndex, |
| | | StrUtil.isEmpty(username) ? "" : StrUtil.format("「{}」", username)); |
| | | try { |
| | | ValidationUtils.validate(BeanUtils.toBean(importUser, UserSaveReqVO.class).setPassword(initPassword)); |
| | | } catch (ConstraintViolationException ex) { |
| | | String key = StrUtil.blankToDefault(importUser.getUsername(), "第 " + currentIndex + " 行"); |
| | | respVO.getFailureUsernames().put(key, ex.getMessage()); |
| | | return; |
| | | } |
| | | // 2.1.2 校验,判断是否有不符合的原因 |
| | | try { |
| | | validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(), |
| | | importUser.getDeptId(), null); |
| | | } catch (ServiceException ex) { |
| | | respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage()); |
| | | return; |
| | | } |
| | | // 2.1.1 解析部门、岗位(支持填名称,部门兼容填编号) |
| | | Long deptId = parseDeptId(importUser.getDept(), deptList); |
| | | Set<Long> 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.2.1 判断如果不存在,在进行插入 |
| | | AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername()); |
| | | if (existUser == null) { |
| | | userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class) |
| | | .setPassword(encodePassword(initPassword)).setPostIds(new HashSet<>())); // 设置默认密码及空岗位编号数组 |
| | | respVO.getCreateUsernames().add(importUser.getUsername()); |
| | | return; |
| | | // 2.2.1 判断如果不存在,进行插入 |
| | | AdminUserDO existUser = userMapper.selectByUsername(username); |
| | | if (existUser == null) { |
| | | // 2.2.2 校验用户账号、手机号、邮箱唯一,以及部门、岗位可用 |
| | | validateUserForCreateOrUpdate(null, username, saveReqVO.getMobile(), saveReqVO.getEmail(), |
| | | deptId, postIds); |
| | | 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.3 如果存在,判断是否允许更新 |
| | | if (!isUpdateSupport) { |
| | | throw new ServiceException(USER_USERNAME_EXISTS.getCode(), |
| | | "用户账号已存在。如需覆盖这个用户,请勾选「是否覆盖」后重新导入;否则请修改 Excel 中的用户账号"); |
| | | } |
| | | // 2.2.4 覆盖时按更新校验(带上用户编号,唯一性校验会跳过这个用户自己) |
| | | validateUserForCreateOrUpdate(existUser.getId(), username, saveReqVO.getMobile(), |
| | | saveReqVO.getEmail(), deptId, postIds); |
| | | 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)); |
| | | } |
| | | // 2.2.2 如果存在,判断是否允许更新 |
| | | if (!isUpdateSupport) { |
| | | respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg()); |
| | | return; |
| | | } |
| | | AdminUserDO updateUser = BeanUtils.toBean(importUser, AdminUserDO.class); |
| | | updateUser.setId(existUser.getId()); |
| | | userMapper.updateById(updateUser); |
| | | respVO.getUpdateUsernames().add(importUser.getUsername()); |
| | | }); |
| | | // 记录导入结果汇总,供 @LogRecord 的 success 文案引用 |
| | | OperateLogUtils.putImportSummary(respVO.getCreateUsernames(), respVO.getUpdateUsernames(), |
| | |
| | | return respVO; |
| | | } |
| | | |
| | | /** Bean 校验失败时的字段名 -> 给用户的修改建议 */ |
| | | private static final Map<String, String> 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<String> 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<DeptDO> 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<DeptDO> 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<Long> parsePostIds(String postText, List<PostDO> postList) { |
| | | if (StrUtil.isEmpty(postText)) { |
| | | throw new ServiceException(USER_IMPORT_POST_EMPTY.getCode(), |
| | | "未填写「岗位」,请填写岗位名称;多个岗位用「、」分隔,例如:普工、班组长"); |
| | | } |
| | | Set<Long> postIds = new HashSet<>(); |
| | | List<String> 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<Long> postIds) { |
| | | if (CollUtil.isEmpty(postIds)) { |
| | | return; |
| | | } |
| | | userPostMapper.insertBatch(convertList(postIds, |
| | | postId -> new UserPostDO().setUserId(userId).setPostId(postId))); |
| | | } |
| | | |
| | | @Override |
| | | public List<AdminUserDO> getUserListByStatus(Integer status) { |
| | | return userMapper.selectListByStatus(status); |