gaoluyang
10 小时以前 46d1b5c5ce9020a1022c3df492685e0d261408df
fix(system): 用户导入模板列名与页面统一,补齐部门/岗位/初始密码列

用户下载模板后在 Excel 里找不到「用户账号」这一列(模板叫「登录名称」,
页面和接口校验叫「用户账号」),列填错后整批 171 行逐行失败。

- 模板列名统一:登录名称→用户账号、用户名称→用户昵称
- 新增「部门」「岗位」「初始密码」列:部门支持填名称(兼容填编号),
岗位填名称、多个用「、」分隔,初始密码留空时取系统配置
- 导入时解析岗位并写入用户-岗位关联,导入出来的用户不再是空岗位
- 逐行提示改成「第 N 行 + 账号 + 原因 + 怎么改」,覆盖账号已存在、
部门/岗位不存在或停用、格式错等情况(不再只回一句「岗位不能为空」)
- 覆盖导入时不修改已有用户的密码

docs/user_import_template_change.md 记录列名对照与接口变更。

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改4个文件
322 ■■■■■ 文件已修改
docs/user_import_template_change.md 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/user_import_template_change.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
# ç”¨æˆ·å¯¼å…¥æ¨¡æ¿åˆ—名变更 - è”调方案
> å˜æ›´æ—¥æœŸï¼š2026-10-16 模块:系统管理 â†’ ç”¨æˆ·ç®¡ç†
## èƒŒæ™¯
用户下载的导入模板里,账号列的表头是「登录名称」,而页面和接口校验里都叫「用户账号」,
用户拿到模板后找不到「用户账号」这一列,填错列后导入 100% å¤±è´¥ï¼ˆé€è¡Œæç¤ºã€Œç”¨æˆ·è´¦å·ä¸èƒ½ä¸ºç©ºã€ï¼‰ã€‚
本次统一列名,并补齐用户实际需要的列。
## æ¶‰åŠé¡µé¢
- ç³»ç»Ÿç®¡ç† â†’ ç”¨æˆ·ç®¡ç† â†’ å¯¼å…¥ç”¨æˆ·ï¼ˆä¸‹è½½å¯¼å…¥æ¨¡æ¿ / å¯¼å…¥ï¼‰
## æŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /system/user/get-import-template | ä¸‹è½½å¯¼å…¥æ¨¡æ¿ï¼ˆåˆ—名、列顺序有变更) |
| POST | /system/user/import | å¯¼å…¥ç”¨æˆ·ï¼ˆå¤±è´¥æ˜Žç»†çš„ key æ ¼å¼æœ‰å˜æ›´ï¼‰ |
### GET /system/user/get-import-template
模板列(顺序即列顺序):
| åˆ— | è¡¨å¤´ | å¿…å¡« | è¯´æ˜Ž |
|----|------|------|------|
| A | ç”¨æˆ·è´¦å· | æ˜¯ | 4-30 ä½æ•°å­—、字母(原表头「登录名称」) |
| B | ç”¨æˆ·æ˜µç§° | å¦ | æœ€å¤š 30 ä¸ªå­—符(原表头「用户名称」) |
| C | éƒ¨é—¨ | å¦ | å¡«éƒ¨é—¨åç§°ï¼›ä¹Ÿå…¼å®¹å¡«éƒ¨é—¨ç¼–号。留空表示暂不归属部门 |
| D | å²—位 | æ˜¯ | å¡«å²—位名称;多个岗位用「、」分隔,如:普工、班组长。岗位需先在系统管理 â†’ å²—位中维护 |
| E | ç”¨æˆ·é‚®ç®± | å¦ | æ ¼å¼éžæ³•或重复时报错,可留空 |
| F | æ‰‹æœºå·ç  | å¦ | 11 ä½æ‰‹æœºå·ï¼Œå¯ç•™ç©º |
| G | ç”¨æˆ·æ€§åˆ« | å¦ | å¡«å­—典标签:男 / å¥³ |
| H | è´¦å·çŠ¶æ€ | å¦ | å¡«å­—典标签:开启 / åœç”¨ï¼›ç•™ç©ºé»˜è®¤å¼€å¯ |
| I | åˆå§‹å¯†ç  | å¦ | 4-16 ä½ï¼›ç•™ç©ºæ—¶ä½¿ç”¨ç³»ç»Ÿé…ç½®çš„初始密码 |
### POST /system/user/import
响应结构不变,仅 `failureUsernames` çš„ key ç”±ã€Œç”¨æˆ·åã€æ”¹ä¸ºã€Œç¬¬ N è¡Œã€Œç”¨æˆ·åã€ã€ï¼Œ
便于用户在 Excel é‡Œå®šä½åˆ°å…·ä½“行:
```json
{
  "code": 0,
  "data": {
    "createUsernames": ["zhangsan"],
    "updateUsernames": [],
    "failureUsernames": {
      "第 3 è¡Œã€Œlisi」": "「岗位」列填写的岗位「车间主任」不存在,请先在「系统管理 â†’ å²—位」中维护,或改成已存在的岗位名称"
    }
  }
}
```
## ä¸šåŠ¡è§„åˆ™
| åœºæ™¯ | è§„则 |
|------|------|
| å²—位 | ä¸Žé¡µé¢ä¸Šæ–°å¢žç”¨æˆ·ä¿æŒä¸€è‡´ï¼Œå¯¼å…¥æ—¶å¿…填;导入成功后会同步写入用户-岗位关联,导入出来的用户可以直接在页面上编辑保存 |
| éƒ¨é—¨ / å²—位名称不存在 | è¯¥è¡Œå¤±è´¥ï¼Œæç¤ºé‡Œç»™å‡ºåç§°å’Œä¸‹ä¸€æ­¥åŠ¨ä½œ |
| éƒ¨é—¨é‡å | è¯¥è¡Œå¤±è´¥ï¼Œæç¤ºæ”¹å¡«éƒ¨é—¨ç¼–号 |
| éƒ¨é—¨ / å²—位已停用 | è¯¥è¡Œå¤±è´¥ï¼Œæç¤ºå…ˆå¯ç”¨ |
| è´¦å·å·²å­˜åœ¨ä¸”未勾选「是否覆盖」 | è¯¥è¡Œå¤±è´¥ï¼Œæç¤ºå‹¾é€‰è¦†ç›–或改账号 |
| å·²å­˜åœ¨ä¸”勾选「是否覆盖」 | æ›´æ–°è¯¥ç”¨æˆ·çš„部门、岗位、邮箱、手机号、性别、状态,**不修改其密码** |
| è¦†ç›–时「账号状态」留空 | ä¿æŒåŽŸçŠ¶æ€ä¸å˜ |
## æ³¨æ„äº‹é¡¹
- åˆ—名变了,旧模板(表头还是「登录名称 / ç”¨æˆ·åç§° / éƒ¨é—¨ç¼–号」)不能再使用,需要重新下载模板;
  æ—§æ¨¡æ¿å¯¼å…¥ä¼šé€è¡Œæç¤ºã€Œç”¨æˆ·è´¦å·å¿…须填写」,不会静默导入成错误数据。
- å¤±è´¥æ˜Žç»†å¯èƒ½ä¸Šç™¾æ¡ï¼Œå‰ç«¯ä»¥å¼¹çª—(弹窗内列表可滚动)展示,成功后仍用轻提示。
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
@@ -224,12 +224,14 @@
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入用户模板")
    public void importTemplate(HttpServletResponse response) throws IOException {
        // æ‰‹åŠ¨åˆ›å»ºå¯¼å‡º demo
        // æ‰‹åŠ¨åˆ›å»ºå¯¼å‡º demo:部门、岗位要填写系统里已经存在的名称;多个岗位用「、」分隔
        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").nickname("张三").dept("综合管理部").post("普通员工")
                        .email("zhangsan@example.com").mobile("15601691300").password("123456")
                        .status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.MALE.getSex()).build(),
                UserImportExcelVO.builder().username("lisi").nickname("李四").dept("综合管理部").post("普通员工、班组长")
                        .email("lisi@example.com").mobile("15601701300").password("123456")
                        .status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build()
        );
        // è¾“出
        ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list);
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java
@@ -11,6 +11,9 @@
/**
 * ç”¨æˆ· Excel å¯¼å…¥ VO
 *
 * åˆ—名与页面上的叫法保持一致(用户账号 / ç”¨æˆ·æ˜µç§°ï¼‰ï¼Œé¿å…ç”¨æˆ·æ‹¿åˆ°æ¨¡æ¿åŽæ‰¾ä¸åˆ°å¯¹åº”列。
 * éƒ¨é—¨ã€å²—位两列填「名称」即可(部门也可以继续填部门编号),导入时会在系统里翻译成编号。
 */
@Data
@Builder
@@ -18,14 +21,17 @@
@NoArgsConstructor
public class UserImportExcelVO {
    @ExcelProperty("登录名称")
    @ExcelProperty("用户账号")
    private String username;
    @ExcelProperty("用户名称")
    @ExcelProperty("用户昵称")
    private String nickname;
    @ExcelProperty("部门编号")
    private Long deptId;
    @ExcelProperty("部门")
    private String dept;
    @ExcelProperty("岗位")
    private String post;
    @ExcelProperty("用户邮箱")
    private String email;
@@ -41,4 +47,7 @@
    @DictFormat(DictTypeConstants.COMMON_STATUS)
    private Integer status;
    @ExcelProperty("初始密码")
    private String password;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -47,6 +47,11 @@
    ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空");
    ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_002_003_010, "该手机号尚未注册");
    ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭");
    ErrorCode USER_IMPORT_DEPT_NOT_EXISTS = new ErrorCode(1_002_003_012, "导入的部门不存在");
    ErrorCode USER_IMPORT_DEPT_NAME_DUPLICATE = new ErrorCode(1_002_003_013, "导入的部门名称存在多个部门");
    ErrorCode USER_IMPORT_POST_NOT_EXISTS = new ErrorCode(1_002_003_014, "导入的岗位不存在");
    ErrorCode USER_IMPORT_POST_EMPTY = new ErrorCode(1_002_003_015, "导入的岗位不能为空");
    ErrorCode USER_IMPORT_MASTER_DATA_DISABLED = new ErrorCode(1_002_003_016, "导入的部门或岗位已被停用");
    // ========== éƒ¨é—¨æ¨¡å— 1-002-004-000 ==========
    ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
@@ -14,6 +14,7 @@
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;
@@ -21,6 +22,7 @@
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;
@@ -515,11 +517,11 @@
        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<>())
@@ -527,40 +529,52 @@
        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.1.3 æ ¡éªŒç”¨æˆ·è´¦å·ã€æ‰‹æœºå·ã€é‚®ç®±å”¯ä¸€ï¼Œä»¥åŠéƒ¨é—¨ã€å²—位可用
                validateUserForCreateOrUpdate(null, username, saveReqVO.getMobile(), saveReqVO.getEmail(),
                        deptId, postIds);
            // 2.2.1 åˆ¤æ–­å¦‚果不存在,在进行插入
            AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername());
                AdminUserDO existUser = userMapper.selectByUsername(username);
            if (existUser == null) {
                userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class)
                        .setPassword(encodePassword(initPassword)).setPostIds(new HashSet<>())); // è®¾ç½®é»˜è®¤å¯†ç åŠç©ºå²—位编号数组
                respVO.getCreateUsernames().add(importUser.getUsername());
                    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) {
                respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg());
                return;
                    throw new ServiceException(USER_USERNAME_EXISTS.getCode(),
                            "用户账号已存在。如需覆盖这个用户,请勾选「是否覆盖」后重新导入;否则请修改 Excel ä¸­çš„用户账号");
            }
            AdminUserDO updateUser = BeanUtils.toBean(importUser, AdminUserDO.class);
            updateUser.setId(existUser.getId());
                AdminUserDO updateUser = BeanUtils.toBean(saveReqVO, AdminUserDO.class)
                        .setId(existUser.getId()).setPassword(null) // è¦†ç›–时不改密码,避免把在用账号的密码改掉
                        .setStatus(importUser.getStatus()); //「账号状态」留空时保持原状态
            userMapper.updateById(updateUser);
            respVO.getUpdateUsernames().add(importUser.getUsername());
                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(),
@@ -568,6 +582,152 @@
        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);