xiaoyi
7 天以前 e3fcc27bfdb23999a2ce1bcf718cd98062cdd870
fix(system): 用户导入部门改为按名称填写,修复照模板导入必然失败

## 问题
用户导入模板要求填「部门编号」(数据库主键 ID),但部门管理页面不展示 ID,
使用者无从得知该填什么。

更严重的是模板示例是芋道官方 demo:
yunai / 超级管理员 / 部门编号 1.0
yuanma / 源码 / 部门编号 2.0

而实际库里的部门 ID 是 100/140/141/142…,根本没有 1 和 2。
照模板填写必然报「当前部门不存在」,两条示例行 100% 失败。

## 修复

### 1. 模板字段改为「部门名称」
UserImportExcelVO:deptId(Long) -> deptName(String)

### 2. 模板示例取真实部门
UserController.importTemplate 不再硬编码官方 demo,
改为查当前系统真实部门名称填充(如「总经办」「市场营销部」)。

### 3. 导入按名称解析
AdminUserServiceImpl:
- 预加载「部门名称 -> 部门」Map
- 按名称精确匹配,匹配不到给出明确提示:
「部门「XXX」不存在,请填写部门管理中的完整部门名称」
- 不兼容数字编号,避免填错列导致静默错配

### 4. 错误提示优化
- 空文件:原「系统异常」500 -> 「请选择要导入的 Excel 文件」
- 新增错误码 USER_IMPORT_FILE_EMPTY / USER_IMPORT_FILE_INVALID

## 验证(生产实测)
模板表头已变为「部门名称」,示例为真实部门;
原样上传模板 → 创建 2 个,失败 0 个(改前 2 个全失败);
空文件上传 → 「请选择要导入的 Excel 文件」(改前系统异常)。
已修改4个文件
125 ■■■■■ 文件已修改
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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);