yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
@@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.enums.common.SexEnum; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import cn.iocoder.yudao.module.system.service.dept.DeptService; import cn.iocoder.yudao.module.system.service.permission.PermissionService; import cn.iocoder.yudao.module.system.service.permission.RoleService; @@ -43,6 +44,8 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_IMPORT_FILE_EMPTY; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_IMPORT_FILE_INVALID; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_POST_NOT_EMPTY; @Tag(name = "管理后台 - 用户") @@ -222,16 +225,21 @@ } @GetMapping("/get-import-template") @Operation(summary = "获得导入用户模板") @Operation(summary = "获得导入用户模板", description = "部门按部门名称填写,示例取自当前系统真实部门") public void importTemplate(HttpServletResponse response) throws IOException { // 手动创建导出 demo // 示例取当前真实部门名称,避免使用者照抄官方 demo 导致「部门不存在」 List<DeptDO> depts = deptService.getDeptList(new DeptListReqVO()); String sampleDept1 = CollUtil.isNotEmpty(depts) ? depts.get(0).getName() : "请填写部门名称"; String sampleDept2 = depts.size() > 1 ? depts.get(1).getName() : sampleDept1; List<UserImportExcelVO> list = Arrays.asList( UserImportExcelVO.builder().username("yunai").deptId(1L).email("yunai@iocoder.cn").mobile("15601691300") .nickname("超级管理员").status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.MALE.getSex()).build(), UserImportExcelVO.builder().username("yuanma").deptId(2L).email("yuanma@iocoder.cn").mobile("15601701300") .nickname("源码").status(CommonStatusEnum.DISABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build() UserImportExcelVO.builder().username("zhangsan").deptName(sampleDept1).email("zhangsan@example.com") .mobile("13800000001").nickname("张三") .status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.MALE.getSex()).build(), UserImportExcelVO.builder().username("lisi").deptName(sampleDept2).email("lisi@example.com") .mobile("13800000002").nickname("李四") .status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build() ); // 输出 ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list); } @@ -244,7 +252,16 @@ @PreAuthorize("@ss.hasPermission('system:user:import')") public CommonResult<UserImportRespVO> importExcel(@RequestParam("file") MultipartFile file, @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception { List<UserImportExcelVO> list = ExcelUtils.read(file, UserImportExcelVO.class); // 空文件给出明确提示,避免底层异常变成「系统异常」 if (file == null || file.isEmpty()) { throw exception(USER_IMPORT_FILE_EMPTY); } List<UserImportExcelVO> list; try { list = ExcelUtils.read(file, UserImportExcelVO.class); } catch (Exception ex) { throw exception(USER_IMPORT_FILE_INVALID); } return success(userService.importUserList(list, updateSupport)); } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java
@@ -11,6 +11,9 @@ /** * 用户 Excel 导入 VO * * 部门改为按「部门名称」填写:原方案要求填写部门编号(数据库主键 ID), * 但部门管理页面并不展示 ID,使用者无从得知该填什么,照模板示例填写必然失败。 */ @Data @Builder @@ -24,8 +27,13 @@ @ExcelProperty("用户名称") private String nickname; @ExcelProperty("部门编号") private Long deptId; /** * 部门名称 * * 按名称匹配部门;同时兼容填写数字(历史模板的部门编号) */ @ExcelProperty("部门名称") private String deptName; @ExcelProperty("用户邮箱") private String email; yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -40,6 +40,8 @@ ErrorCode USER_EMAIL_EXISTS = new ErrorCode(1_002_003_002, "邮箱已经存在"); ErrorCode USER_NOT_EXISTS = new ErrorCode(1_002_003_003, "用户不存在"); ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_002_003_004, "导入用户数据不能为空!"); ErrorCode USER_IMPORT_FILE_EMPTY = new ErrorCode(1_002_003_012, "请选择要导入的 Excel 文件"); ErrorCode USER_IMPORT_FILE_INVALID = new ErrorCode(1_002_003_013, "Excel 文件格式不正确或内容无法解析,请使用系统提供的导入模板"); ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1_002_003_005, "用户密码校验失败"); ErrorCode USER_IS_DISABLE = new ErrorCode(1_002_003_006, "名字为【{}】的用户已被禁用"); ErrorCode USER_POST_NOT_EMPTY = new ErrorCode(1_002_003_007, "岗位不能为空"); yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
@@ -26,6 +26,7 @@ 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.controller.admin.dept.vo.dept.DeptListReqVO; 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; @@ -520,6 +521,12 @@ if (StrUtil.isEmpty(initPassword)) { throw exception(USER_IMPORT_INIT_PASSWORD); } // 1.3 预加载部门:名称 -> 部门,编号 -> 部门,供两种写法解析 List<DeptDO> allDepts = deptService.getDeptList(new DeptListReqVO()); Map<String, DeptDO> deptByName = new HashMap<>(allDepts.size()); for (DeptDO dept : allDepts) { deptByName.put(dept.getName().trim(), dept); } // 2. 遍历,逐个创建 or 更新 UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>()) @@ -527,38 +534,53 @@ AtomicInteger index = new AtomicInteger(1); importUsers.forEach(importUser -> { int currentIndex = index.getAndIncrement(); // 2.1.1 校验字段是否符合要求 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()); // 用于报错的定位 key:优先登录名,否则「第 N 行」 String rowKey = StrUtil.blankToDefault(importUser.getUsername(), "第 " + currentIndex + " 行"); // 2.1 解析部门:优先按名称,兼容历史模板填的部门编号 DeptDO dept = resolveImportDept(importUser.getDeptName(), deptByName); if (dept == null && StrUtil.isNotBlank(importUser.getDeptName())) { respVO.getFailureUsernames().put(rowKey, "部门「" + importUser.getDeptName().trim() + "」不存在,请填写部门管理中的完整部门名称"); return; } // 2.1.2 校验,判断是否有不符合的原因 Long deptId = dept != null ? dept.getId() : null; importUser.setDeptName(dept != null ? dept.getName() : importUser.getDeptName()); // 2.2 校验字段是否符合要求 try { validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(), importUser.getDeptId(), null); ValidationUtils.validate(BeanUtils.toBean(importUser, UserSaveReqVO.class) .setPassword(initPassword).setDeptId(deptId)); } catch (ConstraintViolationException ex) { respVO.getFailureUsernames().put(rowKey, ex.getMessage()); return; } // 2.3 校验业务约束(手机号/邮箱/部门是否存在) try { validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(), deptId, null); } catch (ServiceException ex) { respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage()); respVO.getFailureUsernames().put(rowKey, ex.getMessage()); return; } // 2.2.1 判断如果不存在,在进行插入 // 2.4 判断如果不存在,在进行插入 AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername()); if (existUser == null) { userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class) .setPassword(encodePassword(initPassword)).setPostIds(new HashSet<>())); // 设置默认密码及空岗位编号数组 AdminUserDO newUser = BeanUtils.toBean(importUser, AdminUserDO.class); newUser.setDeptId(deptId); newUser.setPassword(encodePassword(initPassword)).setPostIds(new HashSet<>()); // 默认密码及空岗位 userMapper.insert(newUser); respVO.getCreateUsernames().add(importUser.getUsername()); return; } // 2.2.2 如果存在,判断是否允许更新 // 2.5 如果存在,判断是否允许更新 if (!isUpdateSupport) { respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg()); respVO.getFailureUsernames().put(rowKey, USER_USERNAME_EXISTS.getMsg()); return; } AdminUserDO updateUser = BeanUtils.toBean(importUser, AdminUserDO.class); updateUser.setId(existUser.getId()); updateUser.setDeptId(deptId); userMapper.updateById(updateUser); respVO.getUpdateUsernames().add(importUser.getUsername()); }); @@ -568,6 +590,32 @@ return respVO; } /** * 解析导入行里的部门 * * 优先按部门名称精确匹配;若填写的是纯数字,再按部门编号匹配(兼容历史模板)。 * * @param raw 导入填写的部门文本 * @param byName 部门名称 -> 部门 * @param byId 部门编号 -> 部门 * @return 匹配到的部门,未填写返回 null,填了但匹配不到也返回 null */ /** * 解析导入行里的部门名称 * * 只按部门名称精确匹配(不再兼容数字编号,避免使用者填错列导致静默错配)。 * * @param raw 导入填写的部门名称 * @param byName 部门名称 -> 部门 * @return 匹配到的部门;未填写或匹配不到返回 null */ private DeptDO resolveImportDept(String raw, Map<String, DeptDO> byName) { if (StrUtil.isBlank(raw)) { return null; } return byName.get(raw.trim()); } @Override public List<AdminUserDO> getUserListByStatus(Integer status) { return userMapper.selectListByStatus(status);